diff --git a/.eslintrc.js b/.eslintrc.js index 3059df17f..c1cc4dc4c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,6 +2,7 @@ module.exports = { plugins: [ "matrix-org", "import", + "jsdoc", ], extends: [ "plugin:matrix-org/babel", @@ -45,7 +46,7 @@ module.exports = { // restrict EventEmitters to force callers to use TypedEventEmitter "no-restricted-imports": ["error", { name: "events", - message: "Please use TypedEventEmitter instead" + message: "Please use TypedEventEmitter instead", }], "import/no-restricted-paths": ["error", { @@ -61,6 +62,9 @@ module.exports = { files: [ "**/*.ts", ], + plugins: [ + "eslint-plugin-tsdoc", + ], extends: [ "plugin:matrix-org/typescript", ], @@ -84,6 +88,23 @@ module.exports = { "quotes": "off", // We use a `logger` intermediary module "no-console": "error", + + }, + }, { + // We don't need amazing docs in our spec files + files: [ + "src/**/*.ts", + ], + rules: { + "tsdoc/syntax": "error", + // We use some select jsdoc rules as the tsdoc linter has only one rule + "jsdoc/no-types": "error", + "jsdoc/empty-tags": "error", + "jsdoc/check-property-names": "error", + "jsdoc/check-values": "error", + // These need a bit more work before we can enable + // "jsdoc/check-param-names": "error", + // "jsdoc/check-indentation": "error", }, }, { files: [ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f28b852a7..dd0bfc9eb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,6 @@ -* @matrix-org/element-web - -/src/webrtc @matrix-org/element-call-reviewers -/spec/*/webrtc @matrix-org/element-call-reviewers +* @matrix-org/element-web +/.github/workflows/** @matrix-org/element-web-app-team +/package.json @matrix-org/element-web-app-team +/yarn.lock @matrix-org/element-web-app-team +/src/webrtc @matrix-org/element-call-reviewers +/spec/*/webrtc @matrix-org/element-call-reviewers diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 940aa0ea2..d2a8857aa 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -42,7 +42,7 @@ jobs: if: github.event.action == 'opened' steps: - name: Check membership - uses: tspascoal/get-user-teams-membership@v1 + uses: tspascoal/get-user-teams-membership@v2 id: teams with: username: ${{ github.event.pull_request.user.login }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2655ed6e7..721f45b62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: tag="${{ github.ref_name }}" VERSION="${tag#v}" [ ! -e "$VERSION" ] || rm -r $VERSION - cp -r $RUNNER_TEMP/docs/ $VERSION + cp -r $RUNNER_TEMP/_docs/ $VERSION # Add the new directory to the index if it isn't there already if ! grep -q ">Version $VERSION" index.html; then 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/CHANGELOG.md b/CHANGELOG.md index c835a0105..e5dd9237c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +Changes in [22.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v22.0.0) (2022-12-06) +================================================================================================== + +## 🚨 BREAKING CHANGES + * Enable users to join group calls from multiple devices ([\#2902](https://github.com/matrix-org/matrix-js-sdk/pull/2902)). + +## 🦖 Deprecations + * Deprecate a function containing a typo ([\#2904](https://github.com/matrix-org/matrix-js-sdk/pull/2904)). + +## ✨ Features + * sliding sync: add receipts extension ([\#2912](https://github.com/matrix-org/matrix-js-sdk/pull/2912)). + * Define a spec support policy for the js-sdk ([\#2882](https://github.com/matrix-org/matrix-js-sdk/pull/2882)). + * Further improvements to e2ee logging ([\#2900](https://github.com/matrix-org/matrix-js-sdk/pull/2900)). + * sliding sync: add support for typing extension ([\#2893](https://github.com/matrix-org/matrix-js-sdk/pull/2893)). + * Improve logging on Olm session errors ([\#2885](https://github.com/matrix-org/matrix-js-sdk/pull/2885)). + * Improve logging of e2ee messages ([\#2884](https://github.com/matrix-org/matrix-js-sdk/pull/2884)). + +## 🐛 Bug Fixes + * Fix 3pid invite acceptance not working due to mxid being sent in body ([\#2907](https://github.com/matrix-org/matrix-js-sdk/pull/2907)). Fixes vector-im/element-web#23823. + * Don't hang up calls that haven't started yet ([\#2898](https://github.com/matrix-org/matrix-js-sdk/pull/2898)). + * Read receipt accumulation for threads ([\#2881](https://github.com/matrix-org/matrix-js-sdk/pull/2881)). + * Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)). + * Fix highlight notifications increasing when total notification is zero ([\#2937](https://github.com/matrix-org/matrix-js-sdk/pull/2937)). Fixes vector-im/element-web#23885. + * Fix synthesizeReceipt ([\#2916](https://github.com/matrix-org/matrix-js-sdk/pull/2916)). Fixes vector-im/element-web#23827 vector-im/element-web#23754 and vector-im/element-web#23847. + Changes in [21.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.2.0) (2022-11-22) ================================================================================================== diff --git a/README.md b/README.md index 19592b0ea..bb86a6f94 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk) -Matrix Javascript SDK +Matrix JavaScript SDK ===================== This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a @@ -370,11 +370,14 @@ To build a browser version from scratch when developing:: $ yarn build ``` -To run tests (Jasmine):: +To run tests (Jest): ``` $ yarn test ``` +> **Note** +> The `sync-browserify.spec.ts` requires a browser build (`yarn build`) in order to pass + To run linting: ``` $ yarn lint diff --git a/examples/browser/README.md b/examples/browser/README.md index 1253d8000..45c1d2021 100644 --- a/examples/browser/README.md +++ b/examples/browser/README.md @@ -1,9 +1,9 @@ To try it out, **you must build the SDK first** and then host this folder: ``` - $ npm run build + $ yarn build $ cd examples/browser - $ python -m SimpleHTTPServer 8003 + $ python -m http.server 8003 ``` Then visit ``http://localhost:8003``. diff --git a/examples/browser/browserTest.js b/examples/browser/browserTest.js index e6623b382..891dd176b 100644 --- a/examples/browser/browserTest.js +++ b/examples/browser/browserTest.js @@ -1,11 +1,7 @@ console.log("Loading browser sdk"); -var client = matrixcs.createClient("https://matrix.org"); -client.publicRooms(function (err, data) { - if (err) { - console.error("err %s", JSON.stringify(err)); - return; - } +var client = matrixcs.createClient({baseUrl: "https://matrix.org"}); +client.publicRooms().then(function (data) { console.log("data %s [...]", JSON.stringify(data).substring(0, 100)); console.log("Congratulations! The SDK is working on the browser!"); var result = document.getElementById("result"); diff --git a/package.json b/package.json index 969c71cba..2685e5d28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "21.2.0", + "version": "22.0.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=16.0.0" @@ -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", @@ -54,7 +54,6 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@types/sdp-transform": "^2.4.5", "another-json": "^0.2.0", "bs58": "^5.0.0", "content-type": "^1.0.4", @@ -64,7 +63,8 @@ "p-retry": "4", "qs": "^6.9.6", "sdp-transform": "^2.14.1", - "unhomoglyph": "^1.0.6" + "unhomoglyph": "^1.0.6", + "uuid": "7" }, "devDependencies": { "@babel/cli": "^7.12.10", @@ -80,14 +80,16 @@ "@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", "@types/jest": "^29.0.0", "@types/node": "18", - "@typescript-eslint/eslint-plugin": "^5.6.0", - "@typescript-eslint/parser": "^5.6.0", + "@types/sdp-transform": "^2.4.5", + "@types/uuid": "7", + "@typescript-eslint/eslint-plugin": "^5.45.0", + "@typescript-eslint/parser": "^5.45.0", "allchange": "^1.0.6", "babel-jest": "^29.0.0", "babelify": "^10.0.0", @@ -99,7 +101,9 @@ "eslint-config-google": "^0.14.0", "eslint-import-resolver-typescript": "^3.5.1", "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsdoc": "^39.6.4", "eslint-plugin-matrix-org": "^0.8.0", + "eslint-plugin-tsdoc": "^0.2.17", "eslint-plugin-unicorn": "^45.0.0", "exorcist": "^2.0.0", "fake-indexeddb": "^4.0.0", diff --git a/spec/TestClient.ts b/spec/TestClient.ts index 249f5b39e..8b54fd92a 100644 --- a/spec/TestClient.ts +++ b/spec/TestClient.ts @@ -105,7 +105,7 @@ export class TestClient { /** * stop the client - * @return {Promise} Resolves once the mock http backend has finished all pending flushes + * @returns Promise which resolves once the mock http backend has finished all pending flushes */ public async stop(): Promise { this.client.stopClient(); @@ -135,7 +135,7 @@ export class TestClient { * set up an expectation that the keys will be uploaded, and wait for * that to happen. * - * @returns {Promise} for the one-time keys + * @returns Promise for the one-time keys */ public awaitOneTimeKeyUpload(): Promise> { if (Object.keys(this.oneTimeKeys!).length != 0) { @@ -177,13 +177,13 @@ export class TestClient { * * We check that the query contains each of the users in `response`. * - * @param {Object} response response to the query. + * @param response - response to the query. */ public expectKeyQuery(response: IDownloadKeyResult) { 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; }); @@ -202,7 +202,7 @@ export class TestClient { /** * get the uploaded curve25519 device key * - * @return {string} base64 device key + * @returns base64 device key */ public getDeviceKey(): string { const keyId = 'curve25519:' + this.deviceId; @@ -212,7 +212,7 @@ export class TestClient { /** * get the uploaded ed25519 device key * - * @return {string} base64 device key + * @returns base64 device key */ public getSigningKey(): string { const keyId = 'ed25519:' + this.deviceId; 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..35af27e51 100644 --- a/spec/integ/devicelist-integ.spec.ts +++ b/spec/integ/devicelist-integ.spec.ts @@ -26,11 +26,9 @@ const ROOM_ID = "!room:id"; * get a /sync response which contains a single e2e room (ROOM_ID), with the * members given * - * @param {string[]} roomMembers - * - * @return {object} sync response + * @returns sync response */ -function getSyncResponse(roomMembers) { +function getSyncResponse(roomMembers: string[]) { const stateEvents = [ testUtils.mkEvent({ type: 'm.room.encryption', @@ -43,12 +41,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 +69,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 +93,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 +137,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 +166,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 +204,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 +238,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 +260,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 +291,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 +329,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 +367,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 +395,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 38de34aa5..74366a21b 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,36 +49,29 @@ 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(); - await Promise.all([ - bobTestClient.client.uploadKeys(), - bobTestClient.httpBackend.flushAllExpected(), - ]); + await bobTestClient.httpBackend.flushAllExpected(); expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0); } /** * Set an expectation that querier will query uploader's keys; then flush the http request. * - * @return {promise} resolves once the http request has completed. + * @returns resolves once the http request has completed. */ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise { // can't query keys before bob has uploaded them expect(uploader.deviceKeys).toBeTruthy(); - const uploaderKeys = {}; - uploaderKeys[uploader.deviceId!] = uploader.deviceKeys; + const uploaderKeys: Record = {}; + 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 }; }); @@ -88,13 +83,13 @@ const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient); /** * Set an expectation that ali will claim one of bob's keys; then flush the http request. * - * @return {promise} resolves once the http request has completed. + * @returns resolves once the http request has completed. */ 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 +100,7 @@ async function expectAliClaimKeys(): Promise { } } } - const result = {}; + const result: Record>> = {}; result[bobUserId] = {}; result[bobUserId][bobDeviceId] = {}; result[bobUserId][bobDeviceId][keyId] = keys[keyId]; @@ -156,7 +151,7 @@ const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client) * Ali sends a message, first claiming e2e keys. Set the expectations and * check the results. * - * @return {promise} which resolves to the ciphertext for Bob's device. + * @returns which resolves to the ciphertext for Bob's device. */ async function aliSendsFirstMessage(): Promise { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -173,7 +168,7 @@ async function aliSendsFirstMessage(): Promise { * Ali sends a message without first claiming e2e keys. Set the expectations * and check the results. * - * @return {promise} which resolves to the ciphertext for Bob's device. + * @returns which resolves to the ciphertext for Bob's device. */ async function aliSendsMessage(): Promise { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -188,7 +183,7 @@ async function aliSendsMessage(): Promise { * Bob sends a message, first querying (but not claiming) e2e keys. Set the * expectations and check the results. * - * @return {promise} which resolves to the ciphertext for Ali's device. + * @returns which resolves to the ciphertext for Ali's device. */ async function bobSendsReplyMessage(): Promise { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -203,7 +198,7 @@ async function bobSendsReplyMessage(): Promise { /** * Set an expectation that Ali will send a message, and flush the request * - * @return {promise} which resolves to the ciphertext for Bob's device. + * @returns which resolves to the ciphertext for Bob's device. */ async function expectAliSendMessageRequest(): Promise { const content = await expectSendMessageRequest(aliTestClient.httpBackend); @@ -217,7 +212,7 @@ async function expectAliSendMessageRequest(): Promise { /** * Set an expectation that Bob will send a message, and flush the request * - * @return {promise} which resolves to the ciphertext for Bob's device. + * @returns which resolves to the ciphertext for Bob's device. */ async function expectBobSendMessageRequest(): Promise { const content = await expectSendMessageRequest(bobTestClient.httpBackend); @@ -276,22 +271,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) => { @@ -327,32 +321,32 @@ async function recvMessage( * Send an initial sync response to the client (which just includes the member * list for our test room). * - * @param {TestClient} testClient - * @returns {Promise} which resolves when the sync has been flushed. + * @returns which resolves when the sync has been flushed. */ function firstSync(testClient: TestClient): Promise { // send a sync response including our test room. 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: [], + }, + }, + }, }, }; @@ -385,6 +379,14 @@ describe("MatrixClient crypto", () => { it("Bob uploads device keys", bobUploadsDeviceKeys); + it("handles failures to upload device keys", async () => { + // since device keys are uploaded asynchronously, there's not really much to do here other than fail the + // upload. + bobTestClient.httpBackend.when("POST", "/keys/upload") + .fail(0, new Error("bleh")); + await bobTestClient.httpBackend.flushAllExpected(); + }); + it("Ali downloads Bobs device keys", async () => { await bobUploadsDeviceKeys(); await aliDownloadsKeys(); @@ -424,7 +426,7 @@ describe("MatrixClient crypto", () => { }, }; - const bobKeys = {}; + const bobKeys: Record = {}; bobKeys[bobDeviceId] = bobDeviceKeys; aliTestClient.httpBackend.when( "POST", "/keys/query", @@ -460,7 +462,7 @@ describe("MatrixClient crypto", () => { }, }; - const bobKeys = {}; + const bobKeys: Record = {}; bobKeys[bobDeviceId] = bobDeviceKeys; aliTestClient.httpBackend.when( "POST", "/keys/query", @@ -515,22 +517,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 +608,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', + }, + }), + ], }, - }), - ], + }, + }, }, }; @@ -679,4 +681,45 @@ describe("MatrixClient crypto", () => { }); await httpBackend.flushAllExpected(); }); + + it("Checks for outgoing room key requests for a given event's session", async () => { + const eventA0 = new MatrixEvent({ + sender: "@bob:example.com", + room_id: "!someroom", + content: { + algorithm: 'm.megolm.v1.aes-sha2', + session_id: "sessionid", + sender_key: "senderkey", + }, + }); + const eventA1 = new MatrixEvent({ + sender: "@bob:example.com", + room_id: "!someroom", + content: { + algorithm: 'm.megolm.v1.aes-sha2', + session_id: "sessionid", + sender_key: "senderkey", + }, + }); + const eventB = new MatrixEvent({ + sender: "@bob:example.com", + room_id: "!someroom", + content: { + algorithm: 'm.megolm.v1.aes-sha2', + session_id: "othersessionid", + sender_key: "senderkey", + }, + }); + const nonEncryptedEvent = new MatrixEvent({ + sender: "@bob:example.com", + room_id: "!someroom", + content: {}, + }); + + aliTestClient.client.crypto?.onSyncCompleted({}); + await aliTestClient.client.cancelAndResendEventRoomKeyRequest(eventA0); + expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventA1)).not.toBeNull(); + expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventB)).toBeNull(); + expect(await aliTestClient.client.getOutgoingRoomKeyRequest(nonEncryptedEvent)).toBeNull(); + }); }); diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index fe7393bfd..5bbb82759 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -27,11 +27,13 @@ import { MatrixEvent, PendingEventOrdering, Room, + RoomEvent, } from "../../src/matrix"; import { logger } from "../../src/logger"; import { encodeUri } from "../../src/utils"; import { TestClient } from "../TestClient"; import { FeatureSupport, Thread, THREAD_RELATION_TYPE } from "../../src/models/thread"; +import { emitPromise } from "../test-utils/test-utils"; const userId = "@alice:localhost"; const userName = "Alice"; @@ -534,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 @@ -611,7 +613,12 @@ describe("MatrixClient event timelines", function() { return THREAD_ROOT; }); - httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" + + httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!)) + .respond(200, function() { + return THREAD_ROOT; + }); + + httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" + encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1") .respond(200, function() { @@ -650,7 +657,6 @@ describe("MatrixClient event timelines", function() { encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1") .respond(200, function() { return { - original_event: THREAD_ROOT, chunk: [THREAD_REPLY], // no next batch as this is the oldest end of the timeline }; @@ -660,11 +666,17 @@ describe("MatrixClient event timelines", function() { await httpBackend.flushAllExpected(); const timelineSet = thread.timelineSet; - const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!); - const timeline = await timelinePromise; + httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!)) + .respond(200, function() { + return THREAD_ROOT; + }); - expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy(); - expect(timeline!.getEvents().find(e => e.getId() === THREAD_REPLY.event_id!)).toBeTruthy(); + const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!); + const [timeline] = await Promise.all([timelinePromise, httpBackend.flushAllExpected()]); + + const eventIds = timeline!.getEvents().map(it => it.getId()); + expect(eventIds).toContain(THREAD_ROOT.event_id); + expect(eventIds).toContain(THREAD_REPLY.event_id); }); it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => { @@ -941,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(); @@ -1031,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) { @@ -1083,10 +1095,27 @@ describe("MatrixClient event timelines", function() { return request; } - function respondToContext(): ExpectedHttpRequest { + function respondToThread( + root: Partial, + replies: Partial[], + ): ExpectedHttpRequest { + const request = httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" + + encodeURIComponent(root.event_id!) + "/" + + encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1"); + request.respond(200, function() { + return { + original_event: root, + chunk: [replies], + // no next batch as this is the oldest end of the timeline + }; + }); + return request; + } + + function respondToContext(event: Partial = THREAD_ROOT): ExpectedHttpRequest { const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", { $roomId: roomId, - $eventId: THREAD_ROOT.event_id!, + $eventId: event.event_id!, })); request.respond(200, { end: `${Direction.Forward}${RANDOM_TOKEN}1`, @@ -1094,10 +1123,18 @@ describe("MatrixClient event timelines", function() { state: [], events_before: [], events_after: [], - event: THREAD_ROOT, + event: event, }); return request; } + function respondToEvent(event: Partial = THREAD_ROOT): ExpectedHttpRequest { + const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/event/$eventId", { + $roomId: roomId, + $eventId: event.event_id!, + })); + request.respond(200, event); + return request; + } function respondToMessagesRequest(): ExpectedHttpRequest { const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", { $roomId: roomId, @@ -1183,6 +1220,127 @@ describe("MatrixClient event timelines", function() { expect(myThreads.getPendingEvents()).toHaveLength(0); expect(room.getPendingEvents()).toHaveLength(1); }); + + it("should handle thread updates by reordering the thread list", async () => { + // Test data for a second thread + const THREAD2_ROOT = utils.mkEvent({ + room: roomId, + user: userId, + type: "m.room.message", + content: { + "body": "thread root", + "msgtype": "m.text", + }, + unsigned: { + "m.relations": { + "io.element.thread": { + //"latest_event": undefined, + "count": 1, + "current_user_participated": true, + }, + }, + }, + event: false, + }); + + const THREAD2_REPLY = utils.mkEvent({ + room: roomId, + user: userId, + type: "m.room.message", + content: { + "body": "thread reply", + "msgtype": "m.text", + "m.relates_to": { + // We can't use the const here because we change server support mode for test + rel_type: "io.element.thread", + event_id: THREAD_ROOT.event_id, + }, + }, + event: false, + }); + + // @ts-ignore we know this is a defined path for THREAD ROOT + THREAD2_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD2_REPLY; + + // Test data for a second reply to the first thread + const THREAD_REPLY2 = utils.mkEvent({ + room: roomId, + user: userId, + type: "m.room.message", + content: { + "body": "thread reply", + "msgtype": "m.text", + "m.relates_to": { + // We can't use the const here because we change server support mode for test + rel_type: "io.element.thread", + event_id: THREAD_ROOT.event_id, + }, + }, + event: false, + }); + + // Test data for the first thread, with the second reply + const THREAD_ROOT_UPDATED = { + ...THREAD_ROOT, + unsigned: { + ...THREAD_ROOT.unsigned, + "m.relations": { + ...THREAD_ROOT.unsigned!["m.relations"], + "io.element.thread": { + ...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"], + count: 2, + latest_event: THREAD_REPLY2, + }, + }, + }, + }; + + // Response with test data for the thread list request + const threadsResponse = { + chunk: [THREAD2_ROOT, THREAD_ROOT], + state: [], + next_batch: RANDOM_TOKEN as string | null, + }; + + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Stable); + Thread.setServerSideListSupport(FeatureSupport.Stable); + Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable); + + await client.stopClient(); // we don't need the client to be syncing at this time + const room = client.getRoom(roomId)!; + + // Setup room threads + const timelineSets = await room!.createThreadsTimelineSets(); + expect(timelineSets).not.toBeNull(); + respondToThreads(threadsResponse); + respondToThreads(threadsResponse); + respondToEvent(THREAD_ROOT); + respondToEvent(THREAD_ROOT); + respondToEvent(THREAD2_ROOT); + respondToEvent(THREAD2_ROOT); + respondToThread(THREAD_ROOT, [THREAD_REPLY]); + respondToThread(THREAD2_ROOT, [THREAD2_REPLY]); + await flushHttp(room.fetchRoomThreads()); + const [allThreads] = timelineSets!; + const timeline = allThreads.getLiveTimeline()!; + // Test threads are in chronological order + expect(timeline.getEvents().map(it => it.event.event_id)) + .toEqual([THREAD_ROOT.event_id, THREAD2_ROOT.event_id]); + + // Test adding a second event to the first thread + const thread = room.getThread(THREAD_ROOT.event_id!)!; + const prom = emitPromise(allThreads!, RoomEvent.Timeline); + await thread.addEvent(client.getEventMapper()(THREAD_REPLY2), false); + respondToEvent(THREAD_ROOT_UPDATED); + respondToEvent(THREAD_ROOT_UPDATED); + await httpBackend.flushAllExpected(); + await prom; + // Test threads are in chronological order + expect(timeline!.getEvents().map(it => it.event.event_id)) + .toEqual([THREAD2_ROOT.event_id, THREAD_ROOT.event_id]); + }); }); describe("without server compatibility", function() { @@ -1411,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); @@ -1437,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); @@ -1504,7 +1664,22 @@ describe("MatrixClient event timelines", function() { state: [], end: "end_token", }); - httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" + + httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + + encodeURIComponent(THREAD_ROOT.event_id!)) + .respond(200, function() { + return THREAD_ROOT; + }); + httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + + encodeURIComponent(THREAD_ROOT.event_id!)) + .respond(200, function() { + return THREAD_ROOT; + }); + httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + + encodeURIComponent(THREAD_ROOT.event_id!)) + .respond(200, function() { + return THREAD_ROOT; + }); + httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" + encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Backward, "start_token")) .respond(200, function() { @@ -1513,7 +1688,7 @@ describe("MatrixClient event timelines", function() { chunk: [], }; }); - httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" + + httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" + encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Forward, "end_token")) .respond(200, function() { diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index 5508ceaf0..c585fb3c5 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -511,7 +511,12 @@ describe("MatrixClient", function() { } beforeEach(function() { - return client!.initCrypto(); + // running initCrypto should trigger a key upload + httpBackend!.when("POST", "/keys/upload").respond(200, {}); + return Promise.all([ + client!.initCrypto(), + httpBackend!.flush("/keys/upload", 1), + ]); }); afterEach(() => { @@ -618,13 +623,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([]); @@ -1337,27 +1342,45 @@ describe("MatrixClient", function() { }); }); - describe("registerWithIdentityServer", () => { - it("should pass data to POST request", async () => { - const token = { - access_token: "access_token", - token_type: "Bearer", - matrix_server_name: "server_name", - expires_in: 12345, - }; + describe("setPowerLevel", () => { + it.each([ + { + userId: "alice@localhost", + expectation: { + "alice@localhost": 100, + }, + }, + { + userId: ["alice@localhost", "bob@localhost"], + expectation: { + "alice@localhost": 100, + "bob@localhost": 100, + }, + }, + ])("should modify power levels of $userId correctly", async ({ userId, expectation }) => { + const event = { + getType: () => "m.room.power_levels", + getContent: () => ({ + users: { + "alice@localhost": 50, + }, + }), + } as MatrixEvent; - httpBackend!.when("POST", "/account/register").check(req => { - expect(req.data).toStrictEqual(token); - }).respond(200, { - access_token: "at", - token: "tt", - }); + httpBackend!.when("PUT", "/state/m.room.power_levels").check(req => { + expect(req.data.users).toStrictEqual(expectation); + }).respond(200, {}); - const prom = client!.registerWithIdentityServer(token); + const prom = client!.setPowerLevel("!room_id:server", userId, 100, event); await httpBackend!.flushAllExpected(); - const resp = await prom; - expect(resp.access_token).toBe("at"); - expect(resp.token).toBe("tt"); + await prom; + }); + }); + + describe("uploadKeys", () => { + // uploadKeys() is a no-op nowadays, so there's not much to test here. + it("should complete successfully", async () => { + await client!.uploadKeys(); }); }); }); @@ -1634,7 +1657,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-relations.spec.ts b/spec/integ/matrix-client-relations.spec.ts index 456db2efb..5a4fa2bd0 100644 --- a/spec/integ/matrix-client-relations.spec.ts +++ b/spec/integ/matrix-client-relations.spec.ts @@ -55,7 +55,10 @@ describe("MatrixClient relations", () => { const response = client!.relations(roomId, '$event-0', null, null); httpBackend! - .when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b") + .when("GET", "/rooms/!room%3Ahere/event/%24event-0") + .respond(200, null); + httpBackend! + .when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=b") .respond(200, { chunk: [], next_batch: 'NEXT' }); await httpBackend!.flushAllExpected(); @@ -67,7 +70,10 @@ describe("MatrixClient relations", () => { const response = client!.relations(roomId, '$event-0', 'm.reference', null); httpBackend! - .when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b") + .when("GET", "/rooms/!room%3Ahere/event/%24event-0") + .respond(200, null); + httpBackend! + .when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b") .respond(200, { chunk: [], next_batch: 'NEXT' }); await httpBackend!.flushAllExpected(); @@ -78,10 +84,13 @@ describe("MatrixClient relations", () => { it("should read related events with relation type and event type", async () => { const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message'); + httpBackend! + .when("GET", "/rooms/!room%3Ahere/event/%24event-0") + .respond(200, null); httpBackend! .when( "GET", - "/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b", + "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b", ) .respond(200, { chunk: [], next_batch: 'NEXT' }); @@ -98,10 +107,13 @@ describe("MatrixClient relations", () => { to: 'TO', }); + httpBackend! + .when("GET", "/rooms/!room%3Ahere/event/%24event-0") + .respond(200, null); httpBackend! .when( "GET", - "/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO", + "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO", ) .respond(200, { chunk: [], next_batch: 'NEXT' }); 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..94e2361d7 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -33,8 +33,9 @@ import { IJoinedRoom, IStateEvent, IMinimalEvent, - NotificationCountType, + NotificationCountType, IEphemeral, Room, } from "../../src"; +import { ReceiptType } from '../../src/@types/read_receipts'; import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync'; import * as utils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; @@ -524,105 +525,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 +632,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 +737,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 +828,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 +847,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 +878,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 +909,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 +943,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 +995,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 +1098,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 +1171,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 +1206,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 +1234,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 +1267,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 +1311,47 @@ 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 +1361,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); @@ -1390,6 +1399,9 @@ describe("MatrixClient syncing", () => { rooms: { join: { [roomOne]: { + ephemeral: { + events: [], + }, timeline: { events: [ utils.mkMessage({ @@ -1425,7 +1437,7 @@ describe("MatrixClient syncing", () => { }, }, }, - }; + } as unknown as ISyncResponse; it("should sync unread notifications.", () => { syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = { [THREAD_ID]: { @@ -1448,6 +1460,50 @@ describe("MatrixClient syncing", () => { expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2); }); }); + + it("caches unknown threads receipts and replay them when the thread is created", async () => { + const THREAD_ID = "$unknownthread:localhost"; + + const receipt = { + type: "m.receipt", + room_id: "!foo:bar", + content: { + "$event1:localhost": { + [ReceiptType.Read]: { + "@alice:localhost": { ts: 666, thread_id: THREAD_ID }, + }, + }, + }, + }; + syncData.rooms.join[roomOne].ephemeral.events = [receipt]; + + httpBackend!.when("GET", "/sync").respond(200, syncData); + client!.startClient(); + + return Promise.all([ + httpBackend!.flushAllExpected(), + awaitSyncEvent(), + ]).then(() => { + const room = client?.getRoom(roomOne); + expect(room).toBeInstanceOf(Room); + + expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(true); + + const thread = room!.createThread(THREAD_ID, undefined, [], true); + + expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(false); + + const receipt = thread.getReadReceiptForUserId("@alice:localhost"); + + expect(receipt).toStrictEqual({ + "data": { + "thread_id": "$unknownthread:localhost", + "ts": 666, + }, + "eventId": "$event1:localhost", + }); + }); + }); }); describe("of a room", () => { @@ -1509,18 +1565,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", + }, + }, + }, }, }; @@ -1651,8 +1707,8 @@ describe("MatrixClient syncing", () => { /** * waits for the MatrixClient to emit one or more 'sync' events. * - * @param {Number?} numSyncs number of syncs to wait for - * @returns {Promise} promise which resolves after the sync events have happened + * @param numSyncs - number of syncs to wait for + * @returns promise which resolves after the sync events have happened */ function awaitSyncEvent(numSyncs?: number) { return utils.syncPromise(client!, numSyncs); 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/megolm-integ.spec.ts b/spec/integ/megolm-integ.spec.ts index a4891b702..73a199936 100644 --- a/spec/integ/megolm-integ.spec.ts +++ b/spec/integ/megolm-integ.spec.ts @@ -21,16 +21,19 @@ import * as testUtils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; import { logger } from "../../src/logger"; import { - IContent, - IEvent, IClaimOTKsResult, - IJoinedRoom, - ISyncResponse, + IContent, IDownloadKeyResult, + IEvent, + IJoinedRoom, + IndexedDBCryptoStore, + ISyncResponse, + IUploadKeysRequest, MatrixEvent, MatrixEventEvent, - IndexedDBCryptoStore, Room, + RoomMember, + RoomStateEvent, } from "../../src/matrix"; import { IDeviceKeys } from "../../src/crypto/dehydration"; import { DeviceInfo } from "../../src/crypto/deviceinfo"; @@ -217,8 +220,8 @@ describe("megolm", () => { * Get the device keys for testOlmAccount in a format suitable for a * response to /keys/query * - * @param {string} userId The user ID to query for - * @returns {IDownloadKeyResult} The fake query response + * @param userId - The user ID to query for + * @returns The fake query response */ function getTestKeysQueryResponse(userId: string): IDownloadKeyResult { const testE2eKeys = JSON.parse(testOlmAccount.identity_keys()); @@ -245,8 +248,8 @@ describe("megolm", () => { * Get a one-time key for testOlmAccount in a format suitable for a * response to /keys/claim - * @param {string} userId The user ID to query for - * @returns {IClaimOTKsResult} The fake key claim response + * @param userId - The user ID to query for + * @returns The fake key claim response */ function getTestKeysClaimResponse(userId: string): IClaimOTKsResult { testOlmAccount.generate_one_time_keys(1); @@ -327,7 +330,9 @@ describe("megolm", () => { const room = aliceTestClient.client.getRoom(ROOM_ID)!; const event = room.getLiveTimeline().getEvents()[0]; expect(event.isEncrypted()).toBe(true); - const decryptedEvent = await testUtils.awaitDecryption(event); + + // it probably won't be decrypted yet, because it takes a while to process the olm keys + const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true }); expect(decryptedEvent.getContent().body).toEqual('42'); }); @@ -873,7 +878,12 @@ describe("megolm", () => { const room = aliceTestClient.client.getRoom(ROOM_ID)!; await room.decryptCriticalEvents(); - expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42'); + + // it probably won't be decrypted yet, because it takes a while to process the olm keys + const decryptedEvent = await testUtils.awaitDecryption( + room.getLiveTimeline().getEvents()[0], { waitOnDecryptionFailure: true }, + ); + expect(decryptedEvent.getContent().body).toEqual('42'); const exported = await aliceTestClient.client.exportRoomKeys(); @@ -1012,7 +1022,9 @@ describe("megolm", () => { const room = aliceTestClient.client.getRoom(ROOM_ID)!; const event = room.getLiveTimeline().getEvents()[0]; expect(event.isEncrypted()).toBe(true); - const decryptedEvent = await testUtils.awaitDecryption(event); + + // it probably won't be decrypted yet, because it takes a while to process the olm keys + const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true }); expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID); expect(decryptedEvent.getContent()).toEqual({}); expect(decryptedEvent.getClearContent()).toBeUndefined(); @@ -1364,4 +1376,90 @@ describe("megolm", () => { await beccaTestClient.stop(); }); + + it("allows sending an encrypted event as soon as room state arrives", async () => { + /* Empirically, clients expect to be able to send encrypted events as soon as the + * RoomStateEvent.NewMember notification is emitted, so test that works correctly. + */ + const testRoomId = "!testRoom:id"; + await aliceTestClient.start(); + + aliceTestClient.httpBackend.when("POST", "/keys/query") + .respond(200, function(_path, content: IUploadKeysRequest) { + return { device_keys: {} }; + }); + + /* Alice makes the /createRoom call */ + aliceTestClient.httpBackend.when("POST", "/createRoom") + .respond(200, { room_id: testRoomId }); + await Promise.all([ + aliceTestClient.client.createRoom({ + initial_state: [{ + type: 'm.room.encryption', + state_key: '', + content: { algorithm: 'm.megolm.v1.aes-sha2' }, + }], + }), + aliceTestClient.httpBackend.flushAllExpected(), + ]); + + /* The sync arrives in two parts; first the m.room.create... */ + aliceTestClient.httpBackend.when("GET", "/sync").respond(200, { + rooms: { join: { + [testRoomId]: { + timeline: { events: [ + { + type: 'm.room.create', + state_key: '', + event_id: "$create", + }, + { + type: 'm.room.member', + state_key: aliceTestClient.getUserId(), + content: { membership: "join" }, + event_id: "$alijoin", + }, + ] }, + }, + } }, + }); + await aliceTestClient.flushSync(); + + // ... and then the e2e event and an invite ... + aliceTestClient.httpBackend.when("GET", "/sync").respond(200, { + rooms: { join: { + [testRoomId]: { + timeline: { events: [ + { + type: 'm.room.encryption', + state_key: '', + content: { algorithm: 'm.megolm.v1.aes-sha2' }, + event_id: "$e2e", + }, + { + type: 'm.room.member', + state_key: "@other:user", + content: { membership: "invite" }, + event_id: "$otherinvite", + }, + ] }, + }, + } }, + }); + + // as soon as the roomMember arrives, try to send a message + aliceTestClient.client.on(RoomStateEvent.NewMember, (_e, _s, member: RoomMember) => { + if (member.userId == "@other:user") { + aliceTestClient.client.sendMessage(testRoomId, { msgtype: "m.text", body: "Hello, World" }); + } + }); + + // flush the sync and wait for the /send/ request. + aliceTestClient.httpBackend.when("PUT", "/send/m.room.encrypted/") + .respond(200, (_path, _content) => ({ event_id: "asdfgh" })); + await Promise.all([ + aliceTestClient.flushSync(), + aliceTestClient.httpBackend.flush("/send/m.room.encrypted/", 1), + ]); + }); }); diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 72a7eeaa2..20dfa6141 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -23,7 +23,7 @@ import { TestClient } from "../TestClient"; import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator"; import { MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError, - EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent, + EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent, RoomEvent, Room, IRoomTimelineData, } from "../../src"; import { SlidingSyncSdk } from "../../src/sliding-sync-sdk"; import { SyncState } from "../../src/sync"; @@ -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; } } @@ -170,6 +170,7 @@ describe("SlidingSyncSdk", () => { const roomE = "!e_with_invite:localhost"; const roomF = "!f_calc_room_name:localhost"; const roomG = "!g_join_invite_counts:localhost"; + const roomH = "!g_num_live:localhost"; const data: Record = { [roomA]: { name: "A", @@ -275,6 +276,18 @@ describe("SlidingSyncSdk", () => { invited_count: 2, initial: true, }, + [roomH]: { + name: "H", + required_state: [], + timeline: [ + mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""), + mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId), + mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""), + mkOwnEvent(EventType.RoomMessage, { body: "live event" }), + ], + initial: true, + num_live: 1, + }, }; it("can be created with required_state and timeline", () => { @@ -326,6 +339,33 @@ describe("SlidingSyncSdk", () => { expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count); }); + it("can be created with live events", () => { + let seenLiveEvent = false; + const listener = ( + ev: MatrixEvent, + room?: Room, + toStartOfTimeline?: boolean, + deleted?: boolean, + timelineData?: IRoomTimelineData, + ) => { + if (timelineData?.liveEvent) { + assertTimelineEvents([ev], data[roomH].timeline.slice(-1)); + seenLiveEvent = true; + } + }; + client!.on(RoomEvent.Timeline, listener); + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]); + client!.off(RoomEvent.Timeline, listener); + const gotRoom = client!.getRoom(roomH); + expect(gotRoom).toBeDefined(); + if (gotRoom == null) { return; } + expect(gotRoom.name).toEqual(data[roomH].name); + expect(gotRoom.getMyMembership()).toEqual("join"); + // check the entire timeline is correct + assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), data[roomH].timeline); + expect(seenLiveEvent).toBe(true); + }); + it("can be created with invite_state", () => { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]); const gotRoom = client!.getRoom(roomE); @@ -541,7 +581,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionE2EE", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient({ @@ -607,7 +647,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionAccountData", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient(); @@ -733,7 +773,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionToDevice", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient(); @@ -831,7 +871,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionTyping", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient(); @@ -930,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..d90c0236a 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(() => { @@ -1438,11 +1446,11 @@ function timeout(delayMs: number, reason: string): { promise: Promise, ca /** * Listen until a callback returns data. - * @param {EventEmitter} emitter The event emitter - * @param {string} eventName The event to listen for - * @param {function} callback The callback which will be invoked when events fire. Return something truthy from this to resolve the promise. - * @param {number} timeoutMs The number of milliseconds to wait for the callback to return data. Default: 500ms. - * @returns {Promise} A promise which will be resolved when the callback returns data. If the callback throws or the timeout is reached, + * @param emitter - The event emitter + * @param eventName - The event to listen for + * @param callback - The callback which will be invoked when events fire. Return something truthy from this to resolve the promise. + * @param timeoutMs - The number of milliseconds to wait for the callback to return data. Default: 500ms. + * @returns A promise which will be resolved when the callback returns data. If the callback throws or the timeout is reached, * the promise is rejected. */ function listenUntil( @@ -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 af87ebbe6..814f9b15e 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -13,9 +13,9 @@ import { eventMapperFor } from "../../src/event-mapper"; /** * Return a promise that is resolved when the client next emits a * SYNCING event. - * @param {Object} client The client - * @param {Number=} count Number of syncs to wait for (default 1) - * @return {Promise} Resolves once the client has emitted a SYNCING event + * @param client - The client + * @param count - Number of syncs to wait for (default 1) + * @returns Promise which resolves once the client has emitted a SYNCING event */ export function syncPromise(client: MatrixClient, count = 1): Promise { if (count <= 0) { @@ -41,9 +41,9 @@ export function syncPromise(client: MatrixClient, count = 1): Promise { /** * Create a spy for an object and automatically spy its methods. - * @param {*} constr The class constructor (used with 'new') - * @param {string} name The name of the class - * @return {Object} An instantiated object with spied methods/properties. + * @param constr - The class constructor (used with 'new') + * @param name - The name of the class + * @returns An instantiated object with spied methods/properties. */ export function mock(constr: { new(...args: any[]): T }, name: string): T { // Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/ @@ -84,15 +84,15 @@ interface IEventOpts { let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events /** * Create an Event. - * @param {Object} opts Values for the event. - * @param {string} opts.type The event.type - * @param {string} opts.room The event.room_id - * @param {string} opts.sender The event.sender - * @param {string} opts.skey Optional. The state key (auto inserts empty string) - * @param {Object} opts.content The event.content - * @param {boolean} opts.event True to make a MatrixEvent. - * @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters. - * @return {Object} a JSON object representing this event. + * @param opts - Values for the event. + * @param opts.type - The event.type + * @param opts.room - The event.room_id + * @param opts.sender - The event.sender + * @param opts.skey - Optional. The state key (auto inserts empty string) + * @param opts.content - The event.content + * @param opts.event - True to make a MatrixEvent. + * @param client - If passed along with opts.event=true will be used to set up re-emitters. + * @returns a JSON object representing this event. */ export function mkEvent(opts: IEventOpts & { event: true }, client?: MatrixClient): MatrixEvent; export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): Partial; @@ -160,8 +160,8 @@ interface IPresenceOpts { /** * Create an m.presence event. - * @param {Object} opts Values for the presence. - * @return {Object|MatrixEvent} The event + * @param opts - Values for the presence. + * @returns The event */ export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent; export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial; @@ -193,16 +193,16 @@ interface IMembershipOpts { /** * Create an m.room.member event. - * @param {Object} opts Values for the membership. - * @param {string} opts.room The room ID for the event. - * @param {string} opts.mship The content.membership for the event. - * @param {string} opts.sender The sender user ID for the event. - * @param {string} opts.skey The target user ID for the event if applicable + * @param opts - Values for the membership. + * @param opts.room - The room ID for the event. + * @param opts.mship - The content.membership for the event. + * @param opts.sender - The sender user ID for the event. + * @param opts.skey - The target user ID for the event if applicable * e.g. for invites/bans. - * @param {string} opts.name The content.displayname for the event. - * @param {string} opts.url The content.avatar_url for the event. - * @param {boolean} opts.event True to make a MatrixEvent. - * @return {Object|MatrixEvent} The event + * @param opts.name - The content.displayname for the event. + * @param opts.url - The content.avatar_url for the event. + * @param opts.event - True to make a MatrixEvent. + * @returns The event */ export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent; export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial; @@ -250,13 +250,13 @@ export interface IMessageOpts { /** * Create an m.room.message event. - * @param {Object} opts Values for the message - * @param {string} opts.room The room ID for the event. - * @param {string} opts.user The user ID for the event. - * @param {string} opts.msg Optional. The content.body for the event. - * @param {boolean} opts.event True to make a MatrixEvent. - * @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters. - * @return {Object|MatrixEvent} The event + * @param opts - Values for the message + * @param opts.room - The room ID for the event. + * @param opts.user - The user ID for the event. + * @param opts.msg - Optional. The content.body for the event. + * @param opts.event - True to make a MatrixEvent. + * @param client - If passed along with opts.event=true will be used to set up re-emitters. + * @returns The event */ export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent; export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial; @@ -290,14 +290,14 @@ interface IReplyMessageOpts extends IMessageOpts { /** * Create a reply message. * - * @param {Object} opts Values for the message - * @param {string} opts.room The room ID for the event. - * @param {string} opts.user The user ID for the event. - * @param {string} opts.msg Optional. The content.body for the event. - * @param {MatrixEvent} opts.replyToMessage The replied message - * @param {boolean} opts.event True to make a MatrixEvent. - * @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters. - * @return {Object|MatrixEvent} The event + * @param opts - Values for the message + * @param opts.room - The room ID for the event. + * @param opts.user - The user ID for the event. + * @param opts.msg - Optional. The content.body for the event. + * @param opts.replyToMessage - The replied message + * @param opts.event - True to make a MatrixEvent. + * @param client - If passed along with opts.event=true will be used to set up re-emitters. + * @returns The event */ export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent; export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial; @@ -329,10 +329,8 @@ export function mkReplyMessage( /** * A mock implementation of webstorage - * - * @constructor */ -export class MockStorageApi { +export class MockStorageApi implements Storage { private data: Record = {}; public get length() { @@ -354,30 +352,39 @@ export class MockStorageApi { public removeItem(k: string): void { delete this.data[k]; } + + public clear(): void { + this.data = {}; + } } /** * If an event is being decrypted, wait for it to finish being decrypted. * - * @param {MatrixEvent} event - * @returns {Promise} promise which resolves (to `event`) when the event has been decrypted + * @returns promise which resolves (to `event`) when the event has been decrypted */ -export async function awaitDecryption(event: MatrixEvent): Promise { +export async function awaitDecryption( + event: MatrixEvent, { waitOnDecryptionFailure = false } = {}, +): Promise { // An event is not always decrypted ahead of time // getClearContent is a good signal to know whether an event has been decrypted // already if (event.getClearContent() !== null) { - return event; + if (waitOnDecryptionFailure && event.isDecryptionFailure()) { + logger.log(`${Date.now()} event ${event.getId()} got decryption error; waiting`); + } else { + return event; + } } else { - logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`); - - return new Promise((resolve) => { - event.once(MatrixEventEvent.Decrypted, (ev) => { - logger.log(`${Date.now()} event ${event.getId()} now decrypted`); - resolve(ev); - }); - }); + logger.log(`${Date.now()} event ${event.getId()} is not yet decrypted; waiting`); } + + return new Promise((resolve) => { + event.once(MatrixEventEvent.Decrypted, (ev) => { + logger.log(`${Date.now()} event ${event.getId()} now decrypted`); + resolve(ev); + }); + }); } export const emitPromise = (e: EventEmitter, k: string): Promise => new Promise(r => e.once(k, r)); diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index 8d9423796..84d1f4868 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.ts @@ -26,6 +26,7 @@ import { MatrixClient, MatrixEvent, Room, + RoomMember, RoomState, RoomStateEvent, RoomStateEventHandlerMap, @@ -33,7 +34,7 @@ import { import { TypedEventEmitter } from "../../src/models/typed-event-emitter"; import { ReEmitter } from "../../src/ReEmitter"; import { SyncState } from "../../src/sync"; -import { CallEvent, CallEventHandlerMap, MatrixCall } from "../../src/webrtc/call"; +import { CallEvent, CallEventHandlerMap, CallState, MatrixCall } from "../../src/webrtc/call"; import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler"; import { CallFeed } from "../../src/webrtc/callFeed"; import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall"; @@ -83,6 +84,17 @@ export const DUMMY_SDP = ( export const USERMEDIA_STREAM_ID = "mock_stream_from_media_handler"; export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler"; +export const FAKE_ROOM_ID = "!fake:test.dummy"; +export const FAKE_CONF_ID = "fakegroupcallid"; + +export const FAKE_USER_ID_1 = "@alice:test.dummy"; +export const FAKE_DEVICE_ID_1 = "@AAAAAA"; +export const FAKE_SESSION_ID_1 = "alice1"; +export const FAKE_USER_ID_2 = "@bob:test.dummy"; +export const FAKE_DEVICE_ID_2 = "@BBBBBB"; +export const FAKE_SESSION_ID_2 = "bob1"; +export const FAKE_USER_ID_3 = "@charlie:test.dummy"; + class MockMediaStreamAudioSourceNode { public connect() {} } @@ -103,12 +115,14 @@ export class MockRTCPeerConnection { private negotiationNeededListener?: () => void; public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void; + public iceConnectionStateChangeListener?: () => void; public onTrackListener?: (e: RTCTrackEvent) => void; public needsNegotiation = false; public readyToNegotiate: Promise; private onReadyToNegotiate?: () => void; public localDescription: RTCSessionDescription; public signalingState: RTCSignalingState = "stable"; + public iceConnectionState: RTCIceConnectionState = "connected"; public transceivers: MockRTCRtpTransceiver[] = []; public static triggerAllNegotiations(): void { @@ -144,6 +158,8 @@ export class MockRTCPeerConnection { this.negotiationNeededListener = listener; } else if (type == 'icecandidate') { this.iceCandidateListener = listener; + } else if (type === 'iceconnectionstatechange') { + this.iceConnectionStateChangeListener = listener; } else if (type == 'track') { this.onTrackListener = listener; } @@ -416,6 +432,9 @@ export class MockCallMatrixClient extends TypedEventEmitter().mockReturnValue([]); public getRoom = jest.fn(); + public supportsExperimentalThreads(): boolean { return true; } + public async decryptEventIfNeeded(): Promise {} + public typed(): MatrixClient { return this as unknown as MatrixClient; } public emitRoomState(event: MatrixEvent, state: RoomState): void { @@ -428,6 +447,43 @@ export class MockCallMatrixClient extends TypedEventEmitter { + constructor(public roomId: string, public groupCallId?: string) { + super(); + } + + public state = CallState.Ringing; + public opponentUserId = FAKE_USER_ID_1; + public opponentDeviceId = FAKE_DEVICE_ID_1; + public opponentMember = { userId: this.opponentUserId }; + public callId = "1"; + public localUsermediaFeed = { + setAudioVideoMuted: jest.fn(), + stream: new MockMediaStream("stream"), + }; + public remoteUsermediaFeed?: CallFeed; + public remoteScreensharingFeed?: CallFeed; + + public reject = jest.fn(); + public answerWithCallFeeds = jest.fn(); + public hangup = jest.fn(); + + public sendMetadataUpdate = jest.fn(); + + public on = jest.fn(); + public removeListener = jest.fn(); + + public getOpponentMember(): Partial { + return this.opponentMember; + } + + public getOpponentDeviceId(): string | undefined { + return this.opponentDeviceId; + } + + public typed(): MatrixCall { return this as unknown as MatrixCall; } +} + export class MockCallFeed { constructor( public userId: string, diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index e8a95370c..51d57159d 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"; @@ -19,19 +19,20 @@ import { MemoryStore } from "../../src"; import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager'; import { RoomMember } from '../../src/models/room-member'; import { IStore } from '../../src/store'; +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, @@ -41,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, }, }); @@ -171,7 +172,8 @@ describe("Crypto", function() { }); describe('Session management', function() { - const otkResponse = { + const otkResponse: IClaimOTKsResult = { + failures: {}, one_time_keys: { '@alice:home.server': { aliceDevice: { @@ -187,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; @@ -218,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(); @@ -232,7 +235,7 @@ describe("Crypto", function() { mockRoomList, [], ); - crypto.registerEventHandlers(fakeEmitter); + crypto.registerEventHandlers(fakeEmitter as any); await crypto.init(); }); @@ -244,7 +247,7 @@ describe("Crypto", function() { const prom = new Promise((resolve) => { mockBaseApis.claimOneTimeKeys = function() { resolve(); - return otkResponse; + return Promise.resolve(otkResponse); }; }); @@ -988,16 +991,22 @@ 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"); - await client.client.initCrypto(); + + // running initCrypto should trigger a key upload + client.httpBackend.when("POST", "/keys/upload").respond(200, {}); + await Promise.all([ + client.client.initCrypto(), + client.httpBackend.flush("/keys/upload", 1), + ]); 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) } }, }; }); @@ -1009,18 +1018,24 @@ describe("Crypto", function() { it("encrypts and sends to devices", async () => { client.httpBackend - .when("PUT", "/sendToDevice/m.room.encrypted", { - messages: { - "@bob:example.org": { - bobweb: encryptedPayload, - bobmobile: encryptedPayload, + .when("PUT", "/sendToDevice/m.room.encrypted") + .check((request) => { + const data = request.data; + delete data.messages["@bob:example.org"]["bobweb"]["org.matrix.msgid"]; + delete data.messages["@bob:example.org"]["bobmobile"]["org.matrix.msgid"]; + delete data.messages["@carol:example.org"]["caroldesktop"]["org.matrix.msgid"]; + expect(data).toStrictEqual({ + messages: { + "@bob:example.org": { + bobweb: encryptedPayload, + bobmobile: encryptedPayload, + }, + "@carol:example.org": { + caroldesktop: encryptedPayload, + }, }, - "@carol:example.org": { - caroldesktop: encryptedPayload, - }, - }, - }) - .respond(200, {}); + }); + }).respond(200, {}); await Promise.all([ client.client.encryptAndSendToDevices( @@ -1039,13 +1054,18 @@ 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 - .when("PUT", "/sendToDevice/m.room.encrypted", { + .when("PUT", "/sendToDevice/m.room.encrypted") + .check((req) => { + const data = req.data; + delete data.messages["@bob:example.org"]["bobweb"]["org.matrix.msgid"]; // Carol is nowhere to be seen - messages: { "@bob:example.org": { bobweb: encryptedPayload } }, + expect(data).toStrictEqual({ + messages: { "@bob:example.org": { bobweb: encryptedPayload } }, + }); }) .respond(200, {}); @@ -1140,4 +1160,48 @@ describe("Crypto", function() { await client!.client.crypto!.start(); }); }); + + describe("setRoomEncryption", () => { + let mockClient: MatrixClient; + let mockRoomList: RoomList; + let clientStore: IStore; + let crypto: Crypto; + + beforeEach(async function() { + mockClient = {} as MatrixClient; + const mockStorage = new MockStorageApi() as unknown as Storage; + clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore; + const cryptoStore = new MemoryCryptoStore(); + + mockRoomList = { + getRoomEncryption: jest.fn().mockReturnValue(null), + setRoomEncryption: jest.fn().mockResolvedValue(undefined), + } as unknown as RoomList; + + crypto = new Crypto( + mockClient, + "@alice:home.server", + "FLIBBLE", + clientStore, + cryptoStore, + mockRoomList, + [], + ); + }); + + it("should set the algorithm if called for a known room", async () => { + const room = new Room("!room:id", mockClient, "@my.user:id"); + await clientStore.storeRoom(room); + await crypto.setRoomEncryption("!room:id", { algorithm: "m.megolm.v1.aes-sha2" } as IRoomEncryption); + expect(mockRoomList!.setRoomEncryption).toHaveBeenCalledTimes(1); + expect(jest.mocked(mockRoomList!.setRoomEncryption).mock.calls[0][0]).toEqual("!room:id"); + }); + + it("should raise if called for an unknown room", async () => { + await expect(async () => { + await crypto.setRoomEncryption("!room:id", { algorithm: "m.megolm.v1.aes-sha2" } as IRoomEncryption); + }).rejects.toThrow(/unknown room/); + expect(mockRoomList!.setRoomEncryption).not.toHaveBeenCalled(); + }); + }); }); 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 a1519d4ab..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, @@ -558,7 +563,9 @@ describe("MegolmDecryption", function() { const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0]; expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/); delete contentMap["@bob:example.com"].bobdevice1.session_id; + delete contentMap["@bob:example.com"].bobdevice1["org.matrix.msgid"]; delete contentMap["@bob:example.com"].bobdevice2.session_id; + delete contentMap["@bob:example.com"].bobdevice2["org.matrix.msgid"]; expect(contentMap).toStrictEqual({ '@bob:example.com': { bobdevice1: { @@ -755,6 +762,7 @@ describe("MegolmDecryption", function() { expect(aliceClient.sendToDevice).toHaveBeenCalled(); const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0]; expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/); + delete contentMap["@bob:example.com"]["bobdevice"]["org.matrix.msgid"]; expect(contentMap).toStrictEqual({ '@bob:example.com': { bobdevice: { 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..bd1c71f02 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,32 +103,39 @@ 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({ + const client = new MatrixClient({ baseUrl: "https://my.home.server", idBaseUrl: "https://identity.server", accessToken: "my.access.token", @@ -139,6 +147,10 @@ function makeTestClient(cryptoStore) { cryptoStore: cryptoStore, cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys }, }); + + // initialising the crypto library will trigger a key upload request, which we can stub out + client.uploadKeysRequest = jest.fn(); + return client; } describe("MegolmBackup", function() { @@ -151,36 +163,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 +197,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 +249,7 @@ describe("MegolmBackup", function() { }; mockCrypto.cancelRoomKeyRequest = function() {}; + // @ts-ignore readonly field write mockCrypto.backupManager = { backupGroupSession: jest.fn(), }; @@ -264,21 +275,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 +310,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 +355,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 +367,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 +394,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 +439,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 +451,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 +500,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", @@ -502,6 +513,8 @@ describe("MegolmBackup", function() { deviceId: "device", cryptoStore: cryptoStore, }); + // initialising the crypto library will trigger a key upload request, which we can stub out + client.uploadKeysRequest = jest.fn(); megolmDecryption = new MegolmDecryption({ userId: '@user:id', @@ -509,20 +522,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 +556,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 +593,7 @@ describe("MegolmBackup", function() { }); describe("restore", function() { - let client; + let client: MatrixClient; beforeEach(function() { client = makeTestClient(cryptoStore); @@ -590,8 +604,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 +618,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 +635,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 +652,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 +664,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 +674,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 +694,7 @@ describe("MegolmBackup", function() { { cacheCompleteCallback: resolve }, ); }); - const cachedKey = await client.crypto.getSessionBackupPrivateKey(); + const cachedKey = await client.crypto!.getSessionBackupPrivateKey(); expect(cachedKey).not.toBeNull(); }); @@ -688,7 +703,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 +717,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", @@ -718,6 +730,9 @@ describe("MegolmBackup", function() { deviceId: "device", cryptoStore, }); + // initialising the crypto library will trigger a key upload request, which we can stub out + client.uploadKeysRequest = jest.fn(); + await client.initCrypto(); cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6); diff --git a/spec/unit/crypto/cross-signing.spec.ts b/spec/unit/crypto/cross-signing.spec.ts index e9c112c50..99ad5d0da 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 { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from '../../../src/client'; +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" }, @@ -1129,3 +1137,67 @@ describe("Cross Signing", function() { alice.stopClient(); }); }); + +describe("userHasCrossSigningKeys", function() { + if (!global.Olm) { + return; + } + + beforeAll(() => { + return global.Olm.init(); + }); + + let aliceClient: MatrixClient; + let httpBackend: HttpBackend; + beforeEach(async () => { + const testClient = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); + aliceClient = testClient.client; + httpBackend = testClient.httpBackend; + }); + + afterEach(() => { + aliceClient.stopClient(); + }); + + it("should download devices and return true if one is a cross-signing key", async () => { + httpBackend + .when("POST", "/keys/query") + .respond(200, { + "master_keys": { + "@alice:example.com": { + user_id: "@alice:example.com", + usage: ["master"], + keys: { + "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": + "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", + }, + }, + }, + }); + + let result: boolean; + await Promise.all([ + httpBackend.flush("/keys/query"), + aliceClient.userHasCrossSigningKeys().then((res) => {result = res;}), + ]); + expect(result!).toBeTruthy(); + }); + + it("should download devices and return false if there is no cross-signing key", async () => { + httpBackend + .when("POST", "/keys/query") + .respond(200, {}); + + let result: boolean; + await Promise.all([ + httpBackend.flush("/keys/query"), + aliceClient.userHasCrossSigningKeys().then((res) => {result = res;}), + ]); + expect(result!).toBeFalsy(); + }); + + it("throws an error if crypto is disabled", () => { + aliceClient.crypto = undefined; + expect(() => aliceClient.userHasCrossSigningKeys()).toThrowError("encryption disabled"); + }); +}); 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/setDeviceVerification.spec.ts b/spec/unit/crypto/verification/setDeviceVerification.spec.ts new file mode 100644 index 000000000..3d64153ab --- /dev/null +++ b/spec/unit/crypto/verification/setDeviceVerification.spec.ts @@ -0,0 +1,60 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import '../../../olm-loader'; + +import { CRYPTO_ENABLED, MatrixClient } from "../../../../src/client"; +import { TestClient } from "../../../TestClient"; + +const Olm = global.Olm; + +describe("crypto.setDeviceVerification", () => { + const userId = "@alice:example.com"; + const deviceId1 = "device1"; + let client: MatrixClient; + + if (!CRYPTO_ENABLED) { + return; + } + + beforeAll(async () => { + await Olm.init(); + }); + + beforeEach(async () => { + client = (new TestClient(userId, deviceId1)).client; + await client.initCrypto(); + }); + + it("client should provide crypto", () => { + expect(client.crypto).not.toBeUndefined(); + }); + + describe("when setting an own device as verified", () => { + beforeEach(async () => { + jest.spyOn(client.crypto!, "cancelAndResendAllOutgoingKeyRequests"); + await client.crypto!.setDeviceVerification( + userId, + deviceId1, + true, + ); + }); + + it("cancelAndResendAllOutgoingKeyRequests should be called", () => { + expect(client.crypto!.cancelAndResendAllOutgoingKeyRequests).toHaveBeenCalled(); + }); + }); +}); 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 56124449c..736aeb995 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -17,14 +17,8 @@ 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 { - Method, - ClientPrefix, - IRequestOpts, -} from "../../src/http-api"; -import { QueryDict } from "../../src/utils"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace"; import { EventType, @@ -42,7 +36,18 @@ 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, + ClientPrefix, + Direction, + EventTimeline, ICreateRoomOpts, + IRequestOpts, + MatrixError, + MatrixHttpApi, + MatrixScheduler, + Method, + Room, +} from "../../src"; import { supportsMatrixCall } from "../../src/webrtc/call"; import { makeBeaconEvent } from "../test-utils/beacon"; import { @@ -50,6 +55,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(); @@ -71,17 +79,37 @@ function convertQueryDictToStringRecord(queryDict?: QueryDict): Record); } +type HttpLookup = { + method: string; + path: string; + prefix?: 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: {}, @@ -89,7 +117,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" }, @@ -101,23 +129,14 @@ 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; - prefix?: string; - data?: object; - error?: object; - expectBody?: object; - expectQueryParams?: QueryDict; - thenCall?: Function; - }[] = []; + let httpLookups: HttpLookup[] = []; let acceptKeepalives: boolean; let pendingLookup: { promise: Promise; @@ -128,7 +147,7 @@ describe("MatrixClient", function() { method: Method, path: string, queryParams?: QueryDict, - body?: Body, + body?: BodyInit, requestOpts: IRequestOpts = {}, ) { const { prefix } = requestOpts; @@ -224,24 +243,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)); @@ -265,7 +298,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(); @@ -287,10 +320,10 @@ describe("MatrixClient", function() { }, }]; - await client.timestampToEvent(roomId, 0, 'f'); + await client.timestampToEvent(roomId, 0, Direction.Forward); - expect(client.http.authedRequest.mock.calls.length).toStrictEqual(1); - const [method, path, queryParams,, { prefix }] = client.http.authedRequest.mock.calls[0]; + expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(1); + const [method, path, queryParams,, { prefix }] = mocked(client.http.authedRequest).mock.calls[0]; expect(method).toStrictEqual('GET'); expect(prefix).toStrictEqual(ClientPrefix.V1); expect(path).toStrictEqual( @@ -326,16 +359,16 @@ describe("MatrixClient", function() { }, }]; - await client.timestampToEvent(roomId, 0, 'f'); + await client.timestampToEvent(roomId, 0, Direction.Forward); - expect(client.http.authedRequest.mock.calls.length).toStrictEqual(2); + expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(2); const [ stableMethod, stablePath, stableQueryParams, , { prefix: stablePrefix }, - ] = client.http.authedRequest.mock.calls[0]; + ] = mocked(client.http.authedRequest).mock.calls[0]; expect(stableMethod).toStrictEqual('GET'); expect(stablePrefix).toStrictEqual(ClientPrefix.V1); expect(stablePath).toStrictEqual( @@ -352,7 +385,7 @@ describe("MatrixClient", function() { unstableQueryParams, , { prefix: unstablePrefix }, - ] = client.http.authedRequest.mock.calls[1]; + ] = mocked(client.http.authedRequest).mock.calls[1]; expect(unstableMethod).toStrictEqual('GET'); expect(unstablePrefix).toStrictEqual(unstableMSC3030Prefix); expect(unstablePath).toStrictEqual( @@ -379,10 +412,10 @@ describe("MatrixClient", function() { }, }]; - await expect(client.timestampToEvent(roomId, 0, 'f')).rejects.toBeDefined(); + await expect(client.timestampToEvent(roomId, 0, Direction.Forward)).rejects.toBeDefined(); - expect(client.http.authedRequest.mock.calls.length).toStrictEqual(1); - const [method, path, queryParams,, { prefix }] = client.http.authedRequest.mock.calls[0]; + expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(1); + const [method, path, queryParams,, { prefix }] = mocked(client.http.authedRequest).mock.calls[0]; expect(method).toStrictEqual('GET'); expect(prefix).toStrictEqual(ClientPrefix.V1); expect(path).toStrictEqual( @@ -453,7 +486,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); @@ -493,7 +526,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); @@ -523,7 +556,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, @@ -595,23 +628,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; @@ -655,8 +688,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; @@ -689,8 +722,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; @@ -705,15 +738,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")); @@ -731,10 +764,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(); } }); @@ -750,7 +783,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 : ""); @@ -769,14 +802,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(); }); }); @@ -798,13 +831,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! @@ -822,7 +855,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( @@ -832,7 +865,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(); } }); @@ -848,13 +881,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! @@ -866,8 +899,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, @@ -879,7 +912,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 @@ -890,7 +923,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(); }); @@ -902,7 +935,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(); }); @@ -932,7 +965,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(); }); @@ -940,7 +973,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(); }); @@ -959,7 +992,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(); }); @@ -973,7 +1006,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(); }); @@ -985,7 +1018,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(); }); @@ -1009,7 +1042,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(); }); }); @@ -1078,14 +1111,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) => { @@ -1151,7 +1184,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) { @@ -1168,15 +1201,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(), }); @@ -1187,25 +1219,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); @@ -1267,7 +1300,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; @@ -1306,7 +1339,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 () => { @@ -1314,7 +1347,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/` + @@ -1328,7 +1361,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)}`, @@ -1406,7 +1439,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(); @@ -1414,7 +1447,7 @@ describe("MatrixClient", function() { }; beforeEach(() => { - client.http.authedRequest.mockClear().mockResolvedValue({}); + mocked(client.http.authedRequest).mockClear().mockResolvedValue({}); }); it("no logout_devices specified", async () => { @@ -1453,13 +1486,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`); @@ -1516,11 +1549,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); @@ -1536,7 +1569,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); @@ -1548,7 +1581,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); @@ -1564,7 +1597,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); @@ -1577,7 +1610,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); @@ -1588,9 +1621,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, @@ -1608,24 +1641,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) { @@ -1644,14 +1677,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() }); }; }); @@ -1687,7 +1721,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", }); @@ -1716,7 +1750,7 @@ describe("MatrixClient", function() { roomId: "!snafu:somewhere.org", }); expect(ruleSenderMatch).toBeTruthy(); - expect(ruleSenderMatch.getContent()).toMatchObject({ + expect(ruleSenderMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: REASON, }); @@ -1726,7 +1760,7 @@ describe("MatrixClient", function() { roomId: "!snafu:example.org", }); expect(ruleRoomMatch).toBeTruthy(); - expect(ruleRoomMatch.getContent()).toMatchObject({ + expect(ruleRoomMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: REASON, }); @@ -1751,7 +1785,7 @@ describe("MatrixClient", function() { roomId: BAD_ROOM_ID, }); expect(ruleSenderMatch).toBeTruthy(); - expect(ruleSenderMatch.getContent()).toMatchObject({ + expect(ruleSenderMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: REASON, }); @@ -1785,7 +1819,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", }); @@ -1813,13 +1847,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", @@ -1833,10 +1867,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(); @@ -1850,7 +1884,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(); @@ -1862,7 +1896,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/models/thread.spec.ts b/spec/unit/models/thread.spec.ts index 37e779547..1ffb46637 100644 --- a/spec/unit/models/thread.spec.ts +++ b/spec/unit/models/thread.spec.ts @@ -14,11 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "../../../src/client"; +import { MatrixClient, PendingEventOrdering } from "../../../src/client"; import { Room } from "../../../src/models/room"; -import { Thread } from "../../../src/models/thread"; +import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread"; import { mkThread } from "../../test-utils/thread"; import { TestClient } from "../../TestClient"; +import { emitPromise, mkMessage } from "../../test-utils/test-utils"; +import { EventStatus } from "../../../src"; describe('Thread', () => { describe("constructor", () => { @@ -30,6 +32,50 @@ describe('Thread', () => { }); }); + it("includes pending events in replyCount", async () => { + const myUserId = "@bob:example.org"; + const testClient = new TestClient( + myUserId, + "DEVICE", + "ACCESS_TOKEN", + undefined, + { timelineSupport: false }, + ); + const client = testClient.client; + const room = new Room("123", client, myUserId, { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + jest.spyOn(client, "getRoom").mockReturnValue(room); + + const { thread } = mkThread({ + room, + client, + authorId: myUserId, + participantUserIds: ["@alice:example.org"], + length: 3, + }); + await emitPromise(thread, ThreadEvent.Update); + expect(thread.length).toBe(2); + + const event = mkMessage({ + room: room.roomId, + user: myUserId, + msg: "thread reply", + relatesTo: { + rel_type: THREAD_RELATION_TYPE.name, + event_id: thread.id, + }, + event: true, + }); + await thread.processEvent(event); + event.setStatus(EventStatus.SENDING); + room.addPendingEvent(event, "txn01"); + + await emitPromise(thread, ThreadEvent.Update); + expect(thread.length).toBe(3); + }); + describe("hasUserReadEvent", () => { const myUserId = "@bob:example.org"; let client: MatrixClient; diff --git a/spec/unit/notifications.spec.ts b/spec/unit/notifications.spec.ts index def7ef820..02388f9f0 100644 --- a/spec/unit/notifications.spec.ts +++ b/spec/unit/notifications.spec.ts @@ -37,9 +37,9 @@ let event: MatrixEvent; let threadEvent: MatrixEvent; const ROOM_ID = "!roomId:example.org"; -let THREAD_ID; +let THREAD_ID: string; -function mkPushAction(notify, highlight): IActionsObject { +function mkPushAction(notify: boolean, highlight: boolean): IActionsObject { return { notify, tweaks: { @@ -76,7 +76,7 @@ describe("fixNotificationCountOnDecryption", () => { event: true, }, mockClient); - THREAD_ID = event.getId(); + THREAD_ID = event.getId()!; threadEvent = mkEvent({ type: EventType.RoomMessage, content: { @@ -108,6 +108,16 @@ describe("fixNotificationCountOnDecryption", () => { expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1); }); + it("does not change the room count when there's no unread count", () => { + room.setUnreadNotificationCount(NotificationCountType.Total, 0); + room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); + + fixNotificationCountOnDecryption(mockClient, event); + + expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(1); + expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1); + }); + it("changes the thread count to highlight on decryption", () => { expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1); expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0); @@ -118,6 +128,16 @@ describe("fixNotificationCountOnDecryption", () => { expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1); }); + it("does not change the room count when there's no unread count", () => { + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0); + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0); + + fixNotificationCountOnDecryption(mockClient, event); + + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0); + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0); + }); + it("emits events", () => { const cb = jest.fn(); room.on(RoomEvent.UnreadNotifications, cb); diff --git a/spec/unit/read-receipt.spec.ts b/spec/unit/read-receipt.spec.ts index 2a3fbd87b..df902d13b 100644 --- a/spec/unit/read-receipt.spec.ts +++ b/spec/unit/read-receipt.spec.ts @@ -20,6 +20,7 @@ import { MAIN_ROOM_TIMELINE, ReceiptType } from '../../src/@types/read_receipts' import { MatrixClient } from "../../src/client"; import { Feature, ServerSupport } from '../../src/feature'; import { EventType } from '../../src/matrix'; +import { synthesizeReceipt } from '../../src/models/read-receipt'; import { encodeUri } from '../../src/utils'; import * as utils from "../test-utils/test-utils"; @@ -69,7 +70,7 @@ const roomEvent = utils.mkEvent({ }, }); -function mockServerSideSupport(client, serverSideSupport: ServerSupport) { +function mockServerSideSupport(client: MatrixClient, serverSideSupport: ServerSupport) { client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport); } @@ -175,4 +176,20 @@ describe("Read receipt", () => { await flushPromises(); }); }); + + describe("synthesizeReceipt", () => { + it.each([ + { event: roomEvent, destinationId: MAIN_ROOM_TIMELINE }, + { event: threadEvent, destinationId: threadEvent.threadRootId! }, + ])("adds the receipt to $destinationId", ({ event, destinationId }) => { + const userId = "@bob:example.org"; + const receiptType = ReceiptType.Read; + + const fakeReadReceipt = synthesizeReceipt(userId, event, receiptType); + + const content = fakeReadReceipt.getContent()[event.getId()!][receiptType][userId]; + + expect(content.thread_id).toEqual(destinationId); + }); + }); }); 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..ad74a2853 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -16,30 +16,34 @@ limitations under the License. /** * This is an internal module. See {@link MatrixClient} for the public class. - * @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 +52,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 +135,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 +167,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 +182,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 +197,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 +230,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 +265,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 +274,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 +317,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 +342,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 +376,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 +407,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 +492,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 +510,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 +548,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 +608,7 @@ describe("Room", function() { }); }); - const resetTimelineTests = function(timelineSupport) { + const resetTimelineTests = function(timelineSupport: boolean) { let events: MatrixEvent[]; beforeEach(function() { @@ -630,8 +639,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 +678,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 +699,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 +727,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 +755,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 +768,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 +791,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 +804,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 +1192,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 +1240,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 +1447,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 +1469,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 +1641,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 +1673,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 +2172,7 @@ describe("Room", function() { }, }); - room.createThread("$000", undefined, [eventWithoutARootEvent]); + room.createThread("$000", undefined, [eventWithoutARootEvent], false); const rootEvent = new MatrixEvent({ event_id: "$666", @@ -2188,7 +2190,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 +2408,6 @@ describe("Room", function() { Thread.setServerSideListSupport(FeatureSupport.Stable); room.client.createThreadListMessagesRequest = () => Promise.resolve({ - start: null, - end: null, chunk: [], state: [], }); @@ -2761,7 +2761,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 ee52a518a..eb5d4c18e 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 @@ -1392,7 +1392,7 @@ describe('Call', function() { it("ends call on onHangupReceived() if state is ringing", async () => { expect(call.callHasEnded()).toBe(false); - call.state = CallState.Ringing; + (call as any).state = CallState.Ringing; call.onHangupReceived({} as MCallHangupReject); expect(call.callHasEnded()).toBe(true); @@ -1424,7 +1424,7 @@ describe('Call', function() { )("ends call on onRejectReceived() if in correct state (state=%s)", async (state: CallState) => { expect(call.callHasEnded()).toBe(false); - call.state = state; + (call as any).state = state; call.onRejectReceived({} as MCallHangupReject); expect(call.callHasEnded()).toBe( @@ -1458,4 +1458,50 @@ describe('Call', function() { expect(call.hasPeerConnection).toBe(true); }); }); + + it("should correctly emit LengthChanged", async () => { + const advanceByArray = [2, 3, 5]; + const lengthChangedListener = jest.fn(); + + jest.useFakeTimers(); + call.addListener(CallEvent.LengthChanged, lengthChangedListener); + await fakeIncomingCall(client, call, "1"); + (call.peerConn as unknown as MockRTCPeerConnection).iceConnectionStateChangeListener!(); + + let hasAdvancedBy = 0; + for (const advanceBy of advanceByArray) { + jest.advanceTimersByTime(advanceBy * 1000); + hasAdvancedBy += advanceBy; + + expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy); + expect(lengthChangedListener).toBeCalledWith(hasAdvancedBy); + } + }); + + describe("ICE disconnected timeout", () => { + let mockPeerConn: MockRTCPeerConnection; + + beforeEach(async () => { + jest.useFakeTimers(); + jest.spyOn(call, "hangup"); + + await fakeIncomingCall(client, call, "1"); + + mockPeerConn = (call.peerConn as unknown as MockRTCPeerConnection); + mockPeerConn.iceConnectionState = "disconnected"; + mockPeerConn.iceConnectionStateChangeListener!(); + }); + + it("should hang up after being disconnected for 30 seconds", () => { + jest.advanceTimersByTime(31 * 1000); + expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false); + }); + + it("should not hangup if we've managed to re-connect", () => { + mockPeerConn.iceConnectionState = "connected"; + mockPeerConn.iceConnectionStateChangeListener!(); + jest.advanceTimersByTime(31 * 1000); + expect(call.hangup).not.toHaveBeenCalled(); + }); + }); }); diff --git a/spec/unit/webrtc/callFeed.spec.ts b/spec/unit/webrtc/callFeed.spec.ts index 635fa14fd..e14a1a0c5 100644 --- a/spec/unit/webrtc/callFeed.spec.ts +++ b/spec/unit/webrtc/callFeed.spec.ts @@ -17,13 +17,30 @@ limitations under the License. import { SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes"; import { CallFeed } from "../../../src/webrtc/callFeed"; import { TestClient } from "../../TestClient"; -import { MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc"; +import { MockMatrixCall, MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc"; +import { CallEvent, CallState } from "../../../src/webrtc/call"; describe("CallFeed", () => { - let client; + const roomId = "room1"; + let client: TestClient; + let call: MockMatrixCall; + let feed: CallFeed; beforeEach(() => { client = new TestClient("@alice:foo", "somedevice", "token", undefined, {}); + call = new MockMatrixCall(roomId); + + feed = new CallFeed({ + client: client.client, + call: call.typed(), + roomId, + userId: "user1", + // @ts-ignore Mock + stream: new MockMediaStream("stream1"), + purpose: SDPStreamMetadataPurpose.Usermedia, + audioMuted: false, + videoMuted: false, + }); }); afterEach(() => { @@ -31,21 +48,6 @@ describe("CallFeed", () => { }); describe("muting", () => { - let feed: CallFeed; - - beforeEach(() => { - feed = new CallFeed({ - client, - roomId: "room1", - userId: "user1", - // @ts-ignore Mock - stream: new MockMediaStream("stream1"), - purpose: SDPStreamMetadataPurpose.Usermedia, - audioMuted: false, - videoMuted: false, - }); - }); - describe("muting by default", () => { it("should mute audio by default", () => { expect(feed.isAudioMuted()).toBeTruthy(); @@ -86,4 +88,23 @@ describe("CallFeed", () => { }); }); }); + + describe("connected", () => { + it.each([true, false])("should always be connected, if isLocal()", (val: boolean) => { + // @ts-ignore + feed._connected = val; + jest.spyOn(feed, "isLocal").mockReturnValue(true); + + expect(feed.connected).toBeTruthy(); + }); + + it.each([ + [CallState.Connected, true], + [CallState.Connecting, false], + ])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => { + call.emit(CallEvent.State, state); + + expect(feed.connected).toBe(expected); + }); + }); }); diff --git a/spec/unit/webrtc/groupCall.spec.ts b/spec/unit/webrtc/groupCall.spec.ts index 047474f8e..ad77e15bd 100644 --- a/spec/unit/webrtc/groupCall.spec.ts +++ b/spec/unit/webrtc/groupCall.spec.ts @@ -25,7 +25,7 @@ import { } from '../../../src'; import { RoomStateEvent } from "../../../src/models/room-state"; import { GroupCall, GroupCallEvent, GroupCallState } from "../../../src/webrtc/groupCall"; -import { MatrixClient } from "../../../src/client"; +import { IMyDevice, MatrixClient } from "../../../src/client"; import { installWebRTCMocks, MockCallFeed, @@ -33,6 +33,16 @@ import { MockMediaStream, MockMediaStreamTrack, MockRTCPeerConnection, + MockMatrixCall, + FAKE_ROOM_ID, + FAKE_USER_ID_1, + FAKE_CONF_ID, + FAKE_DEVICE_ID_2, + FAKE_SESSION_ID_2, + FAKE_USER_ID_2, + FAKE_DEVICE_ID_1, + FAKE_SESSION_ID_1, + FAKE_USER_ID_3, } from '../../test-utils/webrtc'; import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes"; import { sleep } from "../../../src/utils"; @@ -41,16 +51,6 @@ import { CallFeed } from '../../../src/webrtc/callFeed'; import { CallEvent, CallState } from '../../../src/webrtc/call'; import { flushPromises } from '../../test-utils/flushPromises'; -const FAKE_ROOM_ID = "!fake:test.dummy"; -const FAKE_CONF_ID = "fakegroupcallid"; - -const FAKE_USER_ID_1 = "@alice:test.dummy"; -const FAKE_DEVICE_ID_1 = "@AAAAAA"; -const FAKE_SESSION_ID_1 = "alice1"; -const FAKE_USER_ID_2 = "@bob:test.dummy"; -const FAKE_DEVICE_ID_2 = "@BBBBBB"; -const FAKE_SESSION_ID_2 = "bob1"; -const FAKE_USER_ID_3 = "@charlie:test.dummy"; const FAKE_STATE_EVENTS = [ { getContent: () => ({ @@ -123,42 +123,6 @@ const createAndEnterGroupCall = async (cli: MatrixClient, room: Room): Promise(), - stream: new MockMediaStream("stream"), - }; - public remoteUsermediaFeed?: CallFeed; - public remoteScreensharingFeed?: CallFeed; - - public reject = jest.fn(); - public answerWithCallFeeds = jest.fn(); - public hangup = jest.fn(); - - public sendMetadataUpdate = jest.fn(); - - public on = jest.fn(); - public removeListener = jest.fn(); - - public getOpponentMember(): Partial { - return this.opponentMember; - } - - public getOpponentDeviceId(): string { - return this.opponentDeviceId; - } - - public typed(): MatrixCall { return this as unknown as MatrixCall; } -} - describe('Group Call', function() { beforeEach(function() { installWebRTCMocks(); @@ -180,13 +144,13 @@ describe('Group Call', function() { room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1); groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt); + room.currentState.members[FAKE_USER_ID_1] = { + userId: FAKE_USER_ID_1, + membership: "join", + } as unknown as RoomMember; }); it("does not initialize local call feed, if it already is", async () => { - room.currentState.members[FAKE_USER_ID_1] = { - userId: FAKE_USER_ID_1, - } as unknown as RoomMember; - await groupCall.initLocalCallFeed(); jest.spyOn(groupCall, "initLocalCallFeed"); await groupCall.enter(); @@ -216,10 +180,6 @@ describe('Group Call', function() { }); it("sends member state event to room on enter", async () => { - room.currentState.members[FAKE_USER_ID_1] = { - userId: FAKE_USER_ID_1, - } as unknown as RoomMember; - await groupCall.create(); try { @@ -249,10 +209,6 @@ describe('Group Call', function() { }); it("sends member state event to room on leave", async () => { - room.currentState.members[FAKE_USER_ID_1] = { - userId: FAKE_USER_ID_1, - } as unknown as RoomMember; - await groupCall.create(); await groupCall.enter(); mockSendState.mockClear(); @@ -267,6 +223,18 @@ describe('Group Call', function() { ); }); + it("includes local device in participants when entered via another session", async () => { + const hasLocalParticipant = () => groupCall.participants.get( + room.getMember(mockClient.getUserId()!)!, + )?.has(mockClient.getDeviceId()!) ?? false; + + expect(groupCall.enteredViaAnotherSession).toBe(false); + expect(hasLocalParticipant()).toBe(false); + + groupCall.enteredViaAnotherSession = true; + expect(hasLocalParticipant()).toBe(true); + }); + it("starts with mic unmuted in regular calls", async () => { try { await groupCall.create(); @@ -347,7 +315,7 @@ describe('Group Call', function() { }); describe("call feeds changing", () => { - let call: MockCall; + let call: MockMatrixCall; const currentFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("current")); const newFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("new")); @@ -357,13 +325,13 @@ describe('Group Call', function() { jest.spyOn(groupCall, "emit"); - call = new MockCall(room.roomId, groupCall.groupCallId); + call = new MockMatrixCall(room.roomId, groupCall.groupCallId); await groupCall.create(); }); it("ignores changes, if we can't get user id of opponent", async () => { - const call = new MockCall(room.roomId, groupCall.groupCallId); + const call = new MockMatrixCall(room.roomId, groupCall.groupCallId); jest.spyOn(call, "getOpponentMember").mockReturnValue({ userId: undefined }); // @ts-ignore Mock @@ -510,10 +478,11 @@ describe('Group Call', function() { }); it("sends metadata updates before unmuting in PTT mode", async () => { - const mockCall = new MockCall(FAKE_ROOM_ID, groupCall.groupCallId); + const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId); + // @ts-ignore groupCall.calls.set( mockCall.getOpponentMember() as RoomMember, - new Map([[mockCall.getOpponentDeviceId(), mockCall.typed()]]), + new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]), ); let metadataUpdateResolve: () => void; @@ -535,10 +504,11 @@ describe('Group Call', function() { }); it("sends metadata updates after muting in PTT mode", async () => { - const mockCall = new MockCall(FAKE_ROOM_ID, groupCall.groupCallId); + const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId); + // @ts-ignore groupCall.calls.set( mockCall.getOpponentMember() as RoomMember, - new Map([[mockCall.getOpponentDeviceId(), mockCall.typed()]]), + new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]), ); // the call starts muted, so unmute to get in the right state to test @@ -694,6 +664,7 @@ describe('Group Call', function() { expect(client1.sendToDevice).toHaveBeenCalled(); + // @ts-ignore const oldCall = groupCall1.calls.get( groupCall1.room.getMember(client2.userId)!, )!.get(client2.deviceId)!; @@ -715,6 +686,7 @@ describe('Group Call', function() { // to even be created... let newCall: MatrixCall | undefined; while ( + // @ts-ignore (newCall = groupCall1.calls.get( groupCall1.room.getMember(client2.userId)!, )?.get(client2.deviceId)) === undefined @@ -759,6 +731,7 @@ describe('Group Call', function() { groupCall1.setMicrophoneMuted(false); groupCall1.setLocalVideoMuted(false); + // @ts-ignore const call = groupCall1.calls.get( groupCall1.room.getMember(client2.userId)!, )!.get(client2.deviceId)!; @@ -870,7 +843,10 @@ describe('Group Call', function() { // It takes a bit of time for the calls to get created await sleep(10); - const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; + // @ts-ignore + const call = groupCall.calls + .get(groupCall.room.getMember(FAKE_USER_ID_2)!)! + .get(FAKE_DEVICE_ID_2)!; call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember; // @ts-ignore Mock call.pushRemoteFeed(new MockMediaStream("stream", [ @@ -893,7 +869,10 @@ describe('Group Call', function() { // It takes a bit of time for the calls to get created await sleep(10); - const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; + // @ts-ignore + const call = groupCall.calls + .get(groupCall.room.getMember(FAKE_USER_ID_2)!)! + .get(FAKE_DEVICE_ID_2)!; call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember; // @ts-ignore Mock call.pushRemoteFeed(new MockMediaStream("stream", [ @@ -935,7 +914,7 @@ describe('Group Call', function() { }); it("ignores incoming calls for other rooms", async () => { - const mockCall = new MockCall("!someotherroom.fake.dummy", groupCall.groupCallId); + const mockCall = new MockMatrixCall("!someotherroom.fake.dummy", groupCall.groupCallId); mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); @@ -944,7 +923,7 @@ describe('Group Call', function() { }); it("rejects incoming calls for the wrong group call", async () => { - const mockCall = new MockCall(room.roomId, "not " + groupCall.groupCallId); + const mockCall = new MockMatrixCall(room.roomId, "not " + groupCall.groupCallId); mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); @@ -952,7 +931,7 @@ describe('Group Call', function() { }); it("ignores incoming calls not in the ringing state", async () => { - const mockCall = new MockCall(room.roomId, groupCall.groupCallId); + const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId); mockCall.state = CallState.Connected; mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); @@ -962,12 +941,13 @@ describe('Group Call', function() { }); it("answers calls for the right room & group call ID", async () => { - const mockCall = new MockCall(room.roomId, groupCall.groupCallId); + const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId); mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); expect(mockCall.reject).not.toHaveBeenCalled(); expect(mockCall.answerWithCallFeeds).toHaveBeenCalled(); + // @ts-ignore expect(groupCall.calls).toEqual(new Map([[ groupCall.room.getMember(FAKE_USER_ID_1)!, new Map([[FAKE_DEVICE_ID_1, mockCall]]), @@ -975,8 +955,8 @@ describe('Group Call', function() { }); it("replaces calls if it already has one with the same user", async () => { - const oldMockCall = new MockCall(room.roomId, groupCall.groupCallId); - const newMockCall = new MockCall(room.roomId, groupCall.groupCallId); + const oldMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId); + const newMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId); newMockCall.opponentMember = oldMockCall.opponentMember; // Ensure referential equality newMockCall.callId = "not " + oldMockCall.callId; @@ -985,6 +965,7 @@ describe('Group Call', function() { expect(oldMockCall.hangup).toHaveBeenCalled(); expect(newMockCall.answerWithCallFeeds).toHaveBeenCalled(); + // @ts-ignore expect(groupCall.calls).toEqual(new Map([[ groupCall.room.getMember(FAKE_USER_ID_1)!, new Map([[FAKE_DEVICE_ID_1, newMockCall]]), @@ -995,7 +976,7 @@ describe('Group Call', function() { // First we leave the call since we have already entered groupCall.leave(); - const call = new MockCall(room.roomId, groupCall.groupCallId); + const call = new MockMatrixCall(room.roomId, groupCall.groupCallId); mockClient.callEventHandler!.calls = new Map([ [call.callId, call.typed()], ]); @@ -1068,7 +1049,10 @@ describe('Group Call', function() { // It takes a bit of time for the calls to get created await sleep(10); - const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; + // @ts-ignore + const call = groupCall.calls + .get(groupCall.room.getMember(FAKE_USER_ID_2)!)! + .get(FAKE_DEVICE_ID_2)!; call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember; call.onNegotiateReceived({ getContent: () => ({ @@ -1270,4 +1254,155 @@ describe('Group Call', function() { }); }); }); + + describe("cleaning member state", () => { + const bobWeb: IMyDevice = { + device_id: "bobweb", + last_seen_ts: 0, + }; + const bobDesktop: IMyDevice = { + device_id: "bobdesktop", + last_seen_ts: 0, + }; + const bobDesktopOffline: IMyDevice = { + device_id: "bobdesktopoffline", + last_seen_ts: 1000 * 60 * 60 * -2, // 2 hours ago + }; + const bobDesktopNeverOnline: IMyDevice = { + device_id: "bobdesktopneveronline", + }; + + const mkContent = (devices: IMyDevice[]) => ({ + "m.calls": [{ + "m.call_id": groupCall.groupCallId, + "m.devices": devices.map(d => ({ + device_id: d.device_id, session_id: "1", feeds: [], expires_ts: 1000 * 60 * 10, + })), + }], + }); + + const expectDevices = (devices: IMyDevice[]) => expect( + room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)?.getContent(), + ).toEqual({ + "m.calls": [{ + "m.call_id": groupCall.groupCallId, + "m.devices": devices.map(d => ({ + device_id: d.device_id, session_id: "1", feeds: [], expires_ts: expect.any(Number), + })), + }], + }); + + let mockClient: MatrixClient; + let room: Room; + let groupCall: GroupCall; + + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(0); + }); + + afterAll(() => jest.useRealTimers()); + + beforeEach(async () => { + const typedMockClient = new MockCallMatrixClient( + FAKE_USER_ID_2, bobWeb.device_id, FAKE_SESSION_ID_2, + ); + jest.spyOn(typedMockClient, "sendStateEvent").mockImplementation( + async (roomId, eventType, content, stateKey) => { + const eventId = `$${Math.random()}`; + if (roomId === room.roomId) { + room.addLiveEvents([new MatrixEvent({ + event_id: eventId, + type: eventType, + room_id: roomId, + sender: FAKE_USER_ID_2, + content, + state_key: stateKey, + })]); + } + return { event_id: eventId }; + }, + ); + mockClient = typedMockClient as unknown as MatrixClient; + + room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_2); + room.getMember = jest.fn().mockImplementation((userId) => ({ userId })); + + groupCall = new GroupCall( + mockClient, + room, + GroupCallType.Video, + false, + GroupCallIntent.Prompt, + FAKE_CONF_ID, + ); + await groupCall.create(); + + mockClient.getDevices = async () => ({ + devices: [ + bobWeb, + bobDesktop, + bobDesktopOffline, + bobDesktopNeverOnline, + ], + }); + }); + + afterEach(() => groupCall.leave()); + + it("doesn't clean up valid devices", async () => { + await groupCall.enter(); + await mockClient.sendStateEvent( + room.roomId, + EventType.GroupCallMemberPrefix, + mkContent([bobWeb, bobDesktop]), + FAKE_USER_ID_2, + ); + + await groupCall.cleanMemberState(); + expectDevices([bobWeb, bobDesktop]); + }); + + it("cleans up our own device if we're disconnected", async () => { + await mockClient.sendStateEvent( + room.roomId, + EventType.GroupCallMemberPrefix, + mkContent([bobWeb, bobDesktop]), + FAKE_USER_ID_2, + ); + + await groupCall.cleanMemberState(); + expectDevices([bobDesktop]); + }); + + it("doesn't clean up the local device if entered via another session", async () => { + groupCall.enteredViaAnotherSession = true; + await mockClient.sendStateEvent( + room.roomId, + EventType.GroupCallMemberPrefix, + mkContent([bobWeb]), + FAKE_USER_ID_2, + ); + + await groupCall.cleanMemberState(); + expectDevices([bobWeb]); + }); + + it("cleans up devices that have never been online", async () => { + await mockClient.sendStateEvent( + room.roomId, + EventType.GroupCallMemberPrefix, + mkContent([bobDesktop, bobDesktopNeverOnline]), + FAKE_USER_ID_2, + ); + + await groupCall.cleanMemberState(); + expectDevices([bobDesktop]); + }); + + it("no-ops if there are no state events", async () => { + await groupCall.cleanMemberState(); + expect(room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)).toBe(null); + }); + }); }); diff --git a/src/@types/IIdentityServerProvider.ts b/src/@types/IIdentityServerProvider.ts index 7b905e316..05793d53a 100644 --- a/src/@types/IIdentityServerProvider.ts +++ b/src/@types/IIdentityServerProvider.ts @@ -18,7 +18,7 @@ export interface IIdentityServerProvider { /** * Gets an access token for use against the identity server, * for the associated client. - * @returns {Promise} Resolves to the access token. + * @returns Promise which resolves to the access token. */ getAccessToken(): Promise; } diff --git a/src/@types/beacon.ts b/src/@types/beacon.ts index 6da17061e..ea8a9c8eb 100644 --- a/src/@types/beacon.ts +++ b/src/@types/beacon.ts @@ -35,7 +35,8 @@ import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location"; * To achieve an arbitrary number of only owner-writable state events * we introduce a variable suffix to the event type * - * Eg + * @example + * ``` * { * "type": "m.beacon_info.@matthew:matrix.org.1", * "state_key": "@matthew:matrix.org", @@ -58,6 +59,7 @@ import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location"; * // more content as described below * } * } + * ``` */ /** @@ -78,20 +80,23 @@ export type MBeaconInfoContent = { /** * m.beacon_info Event example from the spec * https://github.com/matrix-org/matrix-spec-proposals/pull/3672 + * @example + * ``` * { - "type": "m.beacon_info", - "state_key": "@matthew:matrix.org", - "content": { - "m.beacon_info": { - "description": "The Matthew Tracker", // same as an `m.location` description - "timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds - }, - "m.ts": 1436829458432, // creation timestamp of the beacon on the client - "m.asset": { - "type": "m.self" // the type of asset being tracked as per MSC3488 - } - } -} + * "type": "m.beacon_info", + * "state_key": "@matthew:matrix.org", + * "content": { + * "m.beacon_info": { + * "description": "The Matthew Tracker", // same as an `m.location` description + * "timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds + * }, + * "m.ts": 1436829458432, // creation timestamp of the beacon on the client + * "m.asset": { + * "type": "m.self" // the type of asset being tracked as per MSC3488 + * } + * } + * } + * ``` */ /** @@ -107,22 +112,24 @@ export type MBeaconInfoEventContent = & /** * m.beacon event example * https://github.com/matrix-org/matrix-spec-proposals/pull/3672 - * + * @example + * ``` * { - "type": "m.beacon", - "sender": "@matthew:matrix.org", - "content": { - "m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674 - "rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267 - "event_id": "$beacon_info" - }, - "m.location": { - "uri": "geo:51.5008,0.1247;u=35", - "description": "Arbitrary beacon information" - }, - "m.ts": 1636829458432, - } -} + * "type": "m.beacon", + * "sender": "@matthew:matrix.org", + * "content": { + * "m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674 + * "rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267 + * "event_id": "$beacon_info" + * }, + * "m.location": { + * "uri": "geo:51.5008,0.1247;u=35", + * "description": "Arbitrary beacon information" + * }, + * "m.ts": 1636829458432, + * } + * } + * ``` */ /** diff --git a/src/@types/event.ts b/src/@types/event.ts index 168097925..d8bb4cc30 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -120,6 +120,8 @@ export enum RoomType { ElementVideo = "io.element.video", } +export const ToDeviceMessageId = "org.matrix.msgid"; + /** * Identifier for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088) * room purpose. Note that this reference is UNSTABLE and subject to breaking changes, @@ -169,17 +171,21 @@ export const UNSTABLE_MSC2716_MARKER = new UnstableValue("m.room.marker", "org.m * eventual removal. * * Schema (TypeScript): + * ``` * { * service_members?: string[] * } + * ``` * - * Example: + * @example + * ``` * { * "service_members": [ * "@helperbot:localhost", * "@reminderbot:alice.tdl" * ] * } + * ``` */ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue( "io.element.functional_members", 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/@types/requests.ts b/src/@types/requests.ts index f9095455e..75296940a 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -54,17 +54,31 @@ export interface ISendEventResponse { } export interface IPresenceOpts { + // One of "online", "offline" or "unavailable" presence: "online" | "offline" | "unavailable"; + // The status message to attach. status_msg?: string; } export interface IPaginateOpts { + // true to fill backwards, false to go forwards backwards?: boolean; + // number of events to request limit?: number; } export interface IGuestAccessOpts { + /** + * True to allow guests to join this room. This + * implicitly gives guests write access. If false or not given, guests are + * explicitly forbidden from joining the room. + */ allowJoin: boolean; + /** + * True to set history visibility to + * be world_readable. This gives guests read access *from this point forward*. + * If false or not given, history visibility is not modified. + */ allowRead: boolean; } @@ -74,7 +88,9 @@ export interface ISearchOpts { } export interface IEventSearchOpts { + // a JSON filter object to pass in the request filter?: IRoomEventFilter; + // the term to search for term: string; } @@ -92,9 +108,13 @@ export interface ICreateRoomStateEvent { } export interface ICreateRoomOpts { + // The alias localpart to assign to this room. room_alias_name?: string; + // Either 'public' or 'private'. visibility?: Visibility; + // The name to give this room. name?: string; + // The topic to give this room. topic?: string; preset?: Preset; power_level_content_override?: { @@ -111,6 +131,7 @@ export interface ICreateRoomOpts { }; creation_content?: object; initial_state?: ICreateRoomStateEvent[]; + // A list of user IDs to invite to this room. invite?: string[]; invite_3pid?: IInvite3PID[]; is_direct?: boolean; @@ -121,7 +142,10 @@ export interface IRoomDirectoryOptions { server?: string; limit?: number; since?: string; + + // Filter parameters filter?: { + // String to search for generic_search_term?: string; room_types?: Array; }; @@ -153,7 +177,6 @@ export interface IRelationsRequestOpts { } export interface IRelationsResponse { - original_event: IEvent; chunk: IEvent[]; next_batch?: string; prev_batch?: string; diff --git a/src/@types/topic.ts b/src/@types/topic.ts index 0d2708b2e..5b66e07c4 100644 --- a/src/@types/topic.ts +++ b/src/@types/topic.ts @@ -21,10 +21,9 @@ import { UnstableValue } from "../NamespacedValue"; /** * Extensible topic event type based on MSC3765 * https://github.com/matrix-org/matrix-spec-proposals/pull/3765 - */ - -/** - * Eg + * + * @example + * ``` * { * "type": "m.room.topic, * "state_key": "", @@ -39,6 +38,7 @@ import { UnstableValue } from "../NamespacedValue"; * }], * } * } + * ``` */ /** diff --git a/src/ToDeviceMessageQueue.ts b/src/ToDeviceMessageQueue.ts index 0b5b1786a..bf881395b 100644 --- a/src/ToDeviceMessageQueue.ts +++ b/src/ToDeviceMessageQueue.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ToDeviceMessageId } from './@types/event'; import { logger } from "./logger"; import { MatrixError, MatrixClient } from "./matrix"; import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage"; @@ -54,12 +55,15 @@ export class ToDeviceMessageQueue { txnId: this.client.makeTxnId(), }; batches.push(batchWithTxnId); - const recips = batchWithTxnId.batch.map((msg) => `${msg.userId}:${msg.deviceId}`); - logger.info(`Created batch of to-device messages with txn id ${batchWithTxnId.txnId} for ${recips}`); + const msgmap = batchWithTxnId.batch.map( + (msg) => `${msg.userId}/${msg.deviceId} (msgid ${msg.payload[ToDeviceMessageId]})`, + ); + logger.info( + `Enqueuing batch of to-device messages. type=${batch.eventType} txnid=${batchWithTxnId.txnId}`, msgmap, + ); } await this.client.store.saveToDeviceBatches(batches); - logger.info(`Enqueued to-device messages with txn ids ${batches.map((batch) => batch.txnId)}`); this.sendQueue(); } diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index d26e4d5c6..35728d1e2 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -15,8 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** @module auto-discovery */ - import { IClientWellKnown, IWellKnownConfig } from "./client"; import { logger } from './logger'; import { MatrixError, Method, timeoutSignal } from "./http-api"; @@ -49,7 +47,7 @@ interface WellKnownConfig extends Omit { error?: IWellKnownConfig["error"] | null; } -interface ClientConfig { +interface ClientConfig extends Omit { "m.homeserver": WellKnownConfig; "m.identity_server": WellKnownConfig; } @@ -87,8 +85,6 @@ export class AutoDiscovery { /** * The auto discovery failed. The client is expected to communicate * the error to the user and refuse logging in. - * @return {string} - * @constructor */ public static readonly FAIL_ERROR = AutoDiscoveryAction.FAIL_ERROR; @@ -98,8 +94,6 @@ export class AutoDiscovery { * action it would for PROMPT while also warning the user about * what went wrong. The client may also treat this the same as * a FAIL_ERROR state. - * @return {string} - * @constructor */ public static readonly FAIL_PROMPT = AutoDiscoveryAction.FAIL_PROMPT; @@ -107,15 +101,11 @@ export class AutoDiscovery { * The auto discovery didn't fail but did not find anything of * interest. The client is expected to prompt the user for more * information, or fail if it prefers. - * @return {string} - * @constructor */ public static readonly PROMPT = AutoDiscoveryAction.PROMPT; /** * The auto discovery was successful. - * @return {string} - * @constructor */ public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS; @@ -125,13 +115,13 @@ export class AutoDiscovery { * and identity server URL the client would want. Additional details * may also be included, and will be transparently brought into the * response object unaltered. - * @param {object} wellknown The configuration object itself, as returned + * @param wellknown - The configuration object itself, as returned * by the .well-known auto-discovery endpoint. - * @return {Promise} Resolves to the verified + * @returns Promise which resolves to the verified * 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 +175,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 +209,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 +222,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 +247,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 @@ -284,9 +274,9 @@ export class AutoDiscovery { * and identity server URL the client would want. Additional details * may also be discovered, and will be transparently included in the * response object unaltered. - * @param {string} domain The homeserver domain to perform discovery + * @param domain - The homeserver domain to perform discovery * on. For example, "matrix.org". - * @return {Promise} Resolves to the discovered + * @returns Promise which resolves to the discovered * configuration, which may include error states. Rejects on unexpected * failure, not when discovery fails. */ @@ -347,15 +337,15 @@ export class AutoDiscovery { } // Step 2: Validate and parse the config - return AutoDiscovery.fromDiscoveryConfig(wellknown.raw); + return AutoDiscovery.fromDiscoveryConfig(wellknown.raw!); } /** * Gets the raw discovery client configuration for the given domain name. * Should only be used if there's no validation to be done on the resulting * object, otherwise use findClientConfig(). - * @param {string} domain The domain to get the client config for. - * @returns {Promise} Resolves to the domain's client config. Can + * @param domain - The domain to get the client config for. + * @returns Promise which resolves to the domain's client config. Can * be an empty object. */ public static async getRawClientConfig(domain?: string): Promise { @@ -374,11 +364,11 @@ export class AutoDiscovery { * Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and * is suitable for the requirements laid out by .well-known auto discovery. * If valid, the URL will also be stripped of any trailing slashes. - * @param {string} url The potentially invalid URL to sanitize. - * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid. - * @private + * @param url - The potentially invalid URL to sanitize. + * @returns The sanitized URL or a falsey value if the URL is invalid. + * @internal */ - private static sanitizeWellKnownUrl(url: string): string | false { + private static sanitizeWellKnownUrl(url?: string | null): string | false { if (!url) return false; try { @@ -430,9 +420,9 @@ export class AutoDiscovery { * action: One of SUCCESS, IGNORE, or FAIL_PROMPT. * reason: Relatively human-readable description of what went wrong. * error: The actual Error, if one exists. - * @param {string} url The URL to fetch a JSON object from. - * @return {Promise} Resolves to the returned state. - * @private + * @param url - The URL to fetch a JSON object from. + * @returns Promise which resolves to the returned state. + * @internal */ private static async fetchWellKnownObject(url: string): Promise { let response: Response; 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 5b19ef08d..b37362d91 100644 --- a/src/client.ts +++ b/src/client.ts @@ -16,7 +16,6 @@ limitations under the License. /** * This is an internal module. See {@link MatrixClient} for the public class. - * @module client */ import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk"; @@ -78,6 +77,7 @@ import { IMegolmSessionData, isCryptoAvailable, VerificationMethod, + IRoomKeyRequestBody, } from './crypto'; import { DeviceInfo, IDevice } from "./crypto/deviceinfo"; import { decodeRecoveryKey } from './crypto/recoverykey'; @@ -184,7 +184,7 @@ import { RuleId, } from "./@types/PushRules"; import { IThreepid } from "./@types/threepids"; -import { CryptoStore } from "./crypto/store/base"; +import { CryptoStore, OutgoingRoomKeyRequest } from "./crypto/store/base"; import { GroupCall, IGroupCallDataChannelOptions, @@ -257,7 +257,10 @@ export interface ICreateClientOpts { /** * A store to be used for end-to-end crypto session data. If not specified, * end-to-end crypto will be disabled. The `createClient` helper will create - * a default store if needed. + * a default store if needed. Calls the factory supplied to + * {@link setCryptoStoreFactory} if unspecified; or if no factory has been + * specified, uses a default implementation (indexeddb in the browser, + * in-memory otherwise). */ cryptoStore?: CryptoStore; @@ -265,7 +268,7 @@ export interface ICreateClientOpts { * The scheduler to use. If not * specified, this client will not retry requests on failure. This client * will supply its own processing function to - * {@link module:scheduler~MatrixScheduler#setProcessFunction}. + * {@link MatrixScheduler#setProcessFunction}. */ scheduler?: MatrixScheduler; @@ -310,8 +313,8 @@ export interface ICreateClientOpts { /** * Set to true to enable - * improved timeline support ({@link module:client~MatrixClient#getEventTimeline getEventTimeline}). It is - * disabled by default for compatibility with older clients - in particular to + * improved timeline support, see {@link MatrixClient#getEventTimeline}. + * It is disabled by default for compatibility with older clients - in particular to * maintain support for back-paginating the live timeline after a '/sync' * result with a gap. */ @@ -320,7 +323,7 @@ export interface ICreateClientOpts { /** * Extra query parameters to append * to all requests with this client. Useful for application services which require - * ?user_id=. + * `?user_id=`. */ queryParams?: Record; @@ -394,12 +397,12 @@ export enum PendingEventOrdering { export interface IStartClientOpts { /** - * The event limit= to apply to initial sync. Default: 8. + * The event `limit=` to apply to initial sync. Default: 8. */ initialSyncLimit?: number; /** - * True to put archived=true on the /initialSync request. Default: false. + * True to put `archived=true on the /initialSync` request. Default: false. */ includeArchivedRooms?: boolean; @@ -410,8 +413,8 @@ export interface IStartClientOpts { /** * Controls where pending messages appear in a room's timeline. If "chronological", messages will - * appear in the timeline when the call to sendEvent was made. If "detached", - * pending messages will appear in a separate list, accessbile via {@link module:models/room#getPendingEvents}. + * appear in the timeline when the call to `sendEvent` was made. If "detached", + * pending messages will appear in a separate list, accessbile via {@link Room#getPendingEvents}. * Default: "chronological". */ pendingEventOrdering?: PendingEventOrdering; @@ -455,7 +458,14 @@ export interface IStartClientOpts { } export interface IStoredClientOpts extends IStartClientOpts { + // Crypto manager crypto?: Crypto; + /** + * A function which is called + * with a room ID and returns a boolean. It should return 'true' if the SDK can + * SAFELY remove events from this room. It may not be safe to remove events if + * there are other references to the timelines for this room. + */ canResetEntireTimeline: ResetTimelineCallback; } @@ -529,7 +539,7 @@ export interface IPreviewUrlResponse { "matrix:image:size"?: number; } -interface ITurnServerResponse { +export interface ITurnServerResponse { uris: string[]; username: string; password: string; @@ -560,7 +570,7 @@ export interface IClientWellKnown { } export interface IWellKnownConfig { - raw?: any; // todo typings + raw?: IClientWellKnown; action?: AutoDiscoveryAction; reason?: string; error?: Error | string; @@ -604,10 +614,10 @@ interface ITagMetadata { } interface IMessagesResponse { - start: string; - end: string; + start?: string; + end?: string; chunk: IRoomEvent[]; - state: IStateEvent[]; + state?: IStateEvent[]; } interface IThreadedMessagesResponse { @@ -634,6 +644,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; @@ -904,13 +925,184 @@ export type EmittedEvents = ClientEvent | BeaconEvent; export type ClientEventHandlerMap = { + /** + * Fires whenever the SDK's syncing state is updated. The state can be one of: + *
    + * + *
  • PREPARED: The client has synced with the server at least once and is + * ready for methods to be called on it. This will be immediately followed by + * a state of SYNCING. This is the equivalent of "syncComplete" in the + * previous API.
  • + * + *
  • CATCHUP: The client has detected the connection to the server might be + * available again and will now try to do a sync again. As this sync might take + * a long time (depending how long ago was last synced, and general server + * performance) the client is put in this mode so the UI can reflect trying + * to catch up with the server after losing connection.
  • + * + *
  • SYNCING : The client is currently polling for new events from the server. + * This will be called after processing latest events from a sync.
  • + * + *
  • ERROR : The client has had a problem syncing with the server. If this is + * called before PREPARED then there was a problem performing the initial + * sync. If this is called after PREPARED then there was a problem polling + * the server for updates. This may be called multiple times even if the state is + * already ERROR. This is the equivalent of "syncError" in the previous + * API.
  • + * + *
  • RECONNECTING: The sync connection has dropped, but not (yet) in a way that + * should be considered erroneous. + *
  • + * + *
  • STOPPED: The client has stopped syncing with server due to stopClient + * being called. + *
  • + *
+ * State transition diagram: + * ``` + * +---->STOPPED + * | + * +----->PREPARED -------> SYNCING <--+ + * | ^ | ^ | + * | CATCHUP ----------+ | | | + * | ^ V | | + * null ------+ | +------- RECONNECTING | + * | V V | + * +------->ERROR ---------------------+ + * + * NB: 'null' will never be emitted by this event. + * + * ``` + * Transitions: + *
    + * + *
  • `null -> PREPARED` : Occurs when the initial sync is completed + * first time. This involves setting up filters and obtaining push rules. + * + *
  • `null -> ERROR` : Occurs when the initial sync failed first time. + * + *
  • `ERROR -> PREPARED` : Occurs when the initial sync succeeds + * after previously failing. + * + *
  • `PREPARED -> SYNCING` : Occurs immediately after transitioning + * to PREPARED. Starts listening for live updates rather than catching up. + * + *
  • `SYNCING -> RECONNECTING` : Occurs when the live update fails. + * + *
  • `RECONNECTING -> RECONNECTING` : Can occur if the update calls + * continue to fail, but the keepalive calls (to /versions) succeed. + * + *
  • `RECONNECTING -> ERROR` : Occurs when the keepalive call also fails + * + *
  • `ERROR -> SYNCING` : Occurs when the client has performed a + * live update after having previously failed. + * + *
  • `ERROR -> ERROR` : Occurs when the client has failed to keepalive + * for a second time or more.
  • + * + *
  • `SYNCING -> SYNCING` : Occurs when the client has performed a live + * update. This is called after processing.
  • + * + *
  • `* -> STOPPED` : Occurs once the client has stopped syncing or + * trying to sync after stopClient has been called.
  • + *
+ * + * @param state - An enum representing the syncing state. One of "PREPARED", + * "SYNCING", "ERROR", "STOPPED". + * + * @param prevState - An enum representing the previous syncing state. + * One of "PREPARED", "SYNCING", "ERROR", "STOPPED" or null. + * + * @param data - Data about this transition. + * + * @example + * ``` + * matrixClient.on("sync", function(state, prevState, data) { + * switch (state) { + * case "ERROR": + * // update UI to say "Connection Lost" + * break; + * case "SYNCING": + * // update UI to remove any "Connection Lost" message + * break; + * case "PREPARED": + * // the client instance is ready to be queried. + * var rooms = matrixClient.getRooms(); + * break; + * } + * }); + * ``` + */ [ClientEvent.Sync]: (state: SyncState, lastState: SyncState | null, data?: ISyncStateData) => void; + /** + * Fires whenever the SDK receives a new event. + *

+ * This is only fired for live events received via /sync - it is not fired for + * events received over context, search, or pagination APIs. + * + * @param event - The matrix event which caused this event to fire. + * @example + * ``` + * matrixClient.on("event", function(event){ + * var sender = event.getSender(); + * }); + * ``` + */ [ClientEvent.Event]: (event: MatrixEvent) => void; + /** + * Fires whenever the SDK receives a new to-device event. + * @param event - The matrix event which caused this event to fire. + * @example + * ``` + * matrixClient.on("toDeviceEvent", function(event){ + * var sender = event.getSender(); + * }); + * ``` + */ [ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void; + /** + * Fires whenever new user-scoped account_data is added. + * @param event - The event describing the account_data just added + * @param event - The previous account data, if known. + * @example + * ``` + * matrixClient.on("accountData", function(event, oldEvent){ + * myAccountData[event.type] = event.content; + * }); + * ``` + */ [ClientEvent.AccountData]: (event: MatrixEvent, lastEvent?: MatrixEvent) => void; + /** + * Fires whenever a new Room is added. This will fire when you are invited to a + * room, as well as when you join a room. This event is experimental and + * may change. + * @param room - The newly created, fully populated room. + * @example + * ``` + * matrixClient.on("Room", function(room){ + * var roomId = room.roomId; + * }); + * ``` + */ [ClientEvent.Room]: (room: Room) => void; + /** + * Fires whenever a Room is removed. This will fire when you forget a room. + * This event is experimental and may change. + * @param roomId - The deleted room ID. + * @example + * ``` + * matrixClient.on("deleteRoom", function(roomId){ + * // update UI from getRooms() + * }); + * ``` + */ [ClientEvent.DeleteRoom]: (roomId: string) => void; [ClientEvent.SyncUnexpectedError]: (error: Error) => void; + /** + * Fires when the client .well-known info is fetched. + * + * @param data - The JSON object returned by the server + */ [ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void; [ClientEvent.ReceivedVoipEvent]: (event: MatrixEvent) => void; [ClientEvent.TurnServers]: (servers: ITurnServer[]) => void; @@ -1173,10 +1365,10 @@ export class MatrixClient extends TypedEventEmitter { if (this.clientRunning) { @@ -1198,10 +1390,6 @@ export class MatrixClient extends TypedEventEmitter { @@ -1294,9 +1482,9 @@ export class MatrixClient extends TypedEventEmitter} Resolves to undefined if a device could not be dehydrated, or + * @returns Promise which resolves to undefined if a device could not be dehydrated, or * to the new device ID if the dehydration was successful. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Rejects: with an error response. */ public async rehydrateDevice(): Promise { if (this.crypto) { @@ -1371,7 +1559,7 @@ export class MatrixClient extends TypedEventEmitter { try { @@ -1393,12 +1581,12 @@ export class MatrixClient extends TypedEventEmitter} the device id of the newly created dehydrated device + * @returns the device id of the newly created dehydrated device */ public async createDehydratedDevice( key: Uint8Array, @@ -1450,7 +1638,7 @@ export class MatrixClient extends TypedEventEmitter { if (this.clientRunning) { @@ -1469,7 +1657,7 @@ export class MatrixClient extends TypedEventEmitterThis method is experimental * and may change without warning. - * @param {boolean} guest True if this is a guest account. + * @param guest - True if this is a guest account. */ public setGuest(guest: boolean): void { // EXPERIMENTAL: @@ -1689,7 +1874,7 @@ export class MatrixClient extends TypedEventEmitterexplicitly attempts to retry their lost connection. * Will also retry any outbound to-device messages currently in the queue to be sent * (retries of regular outgoing events are handled separately, per-event). - * @return {boolean} True if this resulted in a request being retried. + * @returns True if this resulted in a request being retried. */ public retryImmediately(): boolean { // don't await for this promise: we just want to kick it off @@ -1711,7 +1896,7 @@ export class MatrixClient extends TypedEventEmitter { const now = new Date().getTime(); @@ -1743,13 +1927,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. @@ -1850,11 +2036,17 @@ export class MatrixClient extends TypedEventEmitter[0]); this.crypto = crypto; + + // upload our keys in the background + this.crypto.uploadDeviceKeys().catch((e) => { + // TODO: throwing away this error is a really bad idea. + logger.error("Error uploading device keys", e); + }); } /** * Is end-to-end crypto enabled for this client. - * @return {boolean} True if end-to-end is enabled. + * @returns True if end-to-end is enabled. */ public isCryptoEnabled(): boolean { return !!this.crypto; @@ -1863,7 +2055,7 @@ export class MatrixClient extends TypedEventEmitter} A promise that will resolve when the keys are uploaded. + * @deprecated Does nothing. */ public async uploadKeys(): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - - await this.crypto.uploadDeviceKeys(); + logger.warn("MatrixClient.uploadKeys is deprecated"); } /** * Download the keys for a list of users and stores the keys in the session * store. - * @param {Array} userIds The users to fetch. - * @param {boolean} forceDownload Always download the keys even if cached. + * @param userIds - The users to fetch. + * @param forceDownload - Always download the keys even if cached. * - * @return {Promise} A promise which resolves to a map userId->deviceId->{@link - * module:crypto~DeviceInfo|DeviceInfo}. + * @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo} */ public downloadKeys( userIds: string[], @@ -1914,9 +2100,9 @@ export class MatrixClient extends TypedEventEmitter { const prom = this.setDeviceVerification(userId, deviceId, verified, null, null); @@ -1969,16 +2156,17 @@ export class MatrixClient extends TypedEventEmitter { return this.setDeviceVerification(userId, deviceId, null, blocked, null); @@ -1987,16 +2175,17 @@ export class MatrixClient extends TypedEventEmitter { return this.setDeviceVerification(userId, deviceId, null, null, known); @@ -2018,10 +2207,10 @@ export class MatrixClient extends TypedEventEmitter} resolves to a VerificationRequest + * @returns resolves to a VerificationRequest * when the request has been sent to the other party. */ public requestVerificationDM(userId: string, roomId: string): Promise { @@ -2034,9 +2223,9 @@ export class MatrixClient extends TypedEventEmitter} resolves to a VerificationRequest + * @returns resolves to a VerificationRequest * when the request has been sent to the other party. */ public requestVerification(userId: string, devices?: string[]): Promise { @@ -2079,11 +2268,11 @@ export class MatrixClient extends TypedEventEmitter { @@ -2105,7 +2294,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2252,9 +2438,9 @@ export class MatrixClient extends TypedEventEmitter { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.userHasCrossSigningKeys(); + } + /** * Checks whether cross signing: * - is enabled on this account and trusted by this device @@ -2296,7 +2495,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2315,15 +2514,6 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2339,7 +2529,7 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the number of sessions requiring backup + * @returns Promise which resolves to the number of sessions requiring backup */ public countSessionsNeedingBackup(): Promise { if (!this.crypto) { @@ -2374,8 +2564,8 @@ export class MatrixClient extends TypedEventEmitter} Object with public key metadata, encoded private + * @returns Object with public key metadata, encoded private * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. */ @@ -2416,7 +2606,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2438,7 +2628,6 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2452,14 +2641,14 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2493,9 +2682,9 @@ export class MatrixClient extends TypedEventEmitter { @@ -2510,9 +2699,9 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2526,8 +2715,8 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2574,7 +2763,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2590,9 +2779,9 @@ export class MatrixClient extends TypedEventEmitter} + * @param event - event to be checked */ public async getEventSenderDeviceInfo(event: MatrixEvent): Promise { if (!this.crypto) { @@ -2618,10 +2805,10 @@ export class MatrixClient extends TypedEventEmitter { const device = await this.getEventSenderDeviceInfo(event); @@ -2631,12 +2818,38 @@ export class MatrixClient extends TypedEventEmitter { + if (!this.crypto) { + throw new Error("End-to-End encryption disabled"); + } + const wireContent = event.getWireContent(); + const requestBody: IRoomKeyRequestBody = { + session_id: wireContent.session_id, + sender_key: wireContent.sender_key, + algorithm: wireContent.algorithm, + room_id: event.getRoomId()!, + }; + if ( + !requestBody.session_id + || !requestBody.sender_key + || !requestBody.algorithm + || !requestBody.room_id + ) return Promise.resolve(null); + return this.crypto.cryptoStore.getOutgoingRoomKeyRequest(requestBody); + } + /** * Cancel a room key request for this event if one is ongoing and resend the * request. - * @param {MatrixEvent} event event of which to cancel and resend the room + * @param event - event of which to cancel and resend the room * key request. - * @return {Promise} A promise that will resolve when the key request is queued + * @returns A promise that will resolve when the key request is queued */ public cancelAndResendEventRoomKeyRequest(event: MatrixEvent): Promise { if (!this.crypto) { @@ -2648,9 +2861,9 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2661,8 +2874,8 @@ export class MatrixClient extends TypedEventEmitter} Promise which + * @param payload - fields to include in the encrypted payload + * + * @returns Promise which * resolves once the message has been encrypted and sent to the given - * userDeviceMap, and returns the { contentMap, deviceInfoByDeviceId } + * userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }` * of the successfully sent messages. */ public encryptAndSendToDevices( @@ -2713,7 +2925,7 @@ export class MatrixClient extends TypedEventEmitter { @@ -2742,12 +2954,9 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2760,7 +2969,7 @@ export class MatrixClient extends TypedEventEmitter} Information object from API or null + * @returns Information object from API or null */ public async getKeyBackupVersion(): Promise { let res: IKeyBackupInfo; @@ -2795,14 +3004,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2812,7 +3014,7 @@ export class MatrixClient extends TypedEventEmitter} Resolves when complete. + * @param info - Backup information object as returned by getKeyBackupVersion + * @returns Promise which resolves when complete. */ public enableKeyBackup(info: IKeyBackupInfo): Promise { if (!this.crypto) { @@ -2853,17 +3055,13 @@ export class MatrixClient extends TypedEventEmitter} Object that can be passed to createKeyBackupVersion and + * @returns Object that can be passed to createKeyBackupVersion and * additionally has a 'recovery_key' member with the user-facing recovery key string. */ - // TODO: Verify types public async prepareKeyBackupVersion( password?: string | Uint8Array | null, opts: IKeyBackupPrepareOpts = { secureSecretStorage: false }, @@ -2892,7 +3090,7 @@ export class MatrixClient extends TypedEventEmitter} map of key name to key info the secret is + * @returns map of key name to key info the secret is * encrypted with, or null if it is not present or not encrypted with a * trusted key */ @@ -2904,8 +3102,8 @@ export class MatrixClient extends TypedEventEmitter} Object with 'version' param indicating the version created + * @param info - Info object from prepareKeyBackupVersion + * @returns Object with 'version' param indicating the version created */ public async createKeyBackupVersion(info: IKeyBackupInfo): Promise { if (!this.crypto) { @@ -2999,11 +3197,11 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the number of sessions requiring a backup. + * @returns Promise which resolves to the number of sessions requiring a backup. */ public flagAllGroupSessionsForBackup(): Promise { if (!this.crypto) { @@ -3081,9 +3279,9 @@ export class MatrixClient extends TypedEventEmitter} key backup key + * @param password - Passphrase + * @param backupInfo - Backup metadata from `checkKeyBackup` + * @returns key backup key */ public keyBackupKeyFromPassword(password: string, backupInfo: IKeyBackupInfo): Promise { return keyFromAuthData(backupInfo.auth_data, password); @@ -3095,8 +3293,8 @@ export class MatrixClient extends TypedEventEmitter} Status of restoration with `total` and `imported` + * @param backupInfo - Backup metadata from `checkKeyBackup` + * @param opts - Optional params such as callbacks + * @returns Status of restoration with `total` and `imported` * key counts. */ public async restoreKeyBackupWithPassword( @@ -3151,13 +3349,13 @@ export class MatrixClient extends TypedEventEmitter} Status of restoration with `total` and `imported` + * @param opts - Optional params such as callbacks + * @returns Status of restoration with `total` and `imported` * key counts. */ public async restoreKeyBackupWithSecretStorage( @@ -3186,15 +3384,15 @@ export class MatrixClient extends TypedEventEmitter} Status of restoration with `total` and `imported` + * @returns Status of restoration with `total` and `imported` * key counts. */ public restoreKeyBackupWithRecoveryKey( @@ -3202,28 +3400,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); @@ -3398,8 +3596,8 @@ export class MatrixClient extends TypedEventEmitter { @@ -3415,7 +3613,7 @@ export class MatrixClient extends TypedEventEmitter = {}; for (const [userId, devices] of Object.entries(deviceInfos)) { devicesByUser[userId] = Object.values(devices); } @@ -3431,7 +3629,7 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest( @@ -3446,8 +3644,8 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/account_data/$type", { @@ -3533,8 +3731,8 @@ export class MatrixClient extends TypedEventEmitter(eventType: string): Promise { if (this.isInitialSyncComplete()) { @@ -3574,7 +3772,7 @@ export class MatrixClient extends TypedEventEmitter { - const content = { ignored_users: {} }; + const content = { ignored_users: {} as Record }; userIds.forEach((u) => { content.ignored_users[u] = {}; }); @@ -3598,8 +3796,8 @@ export class MatrixClient extends TypedEventEmitterreturned Room object will have no current state. - * Default: true. - * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, the signing URL is passed in this parameter. - * @param {string[]} opts.viaServers The server names to try and join through in addition to those that are automatically chosen. - * @return {Promise} Resolves: Room object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param roomIdOrAlias - The room ID or room alias to join. + * @param opts - Options when joining the room. + * @returns Promise which resolves: Room object. + * @returns Rejects: with an error response. */ public async joinRoom(roomIdOrAlias: string, opts: IJoinRoomOpts = {}): Promise { if (opts.syncRoom === undefined) { @@ -3665,11 +3858,11 @@ export class MatrixClient extends TypedEventEmitter { // also kick the to-device queue to retry @@ -3682,7 +3875,7 @@ export class MatrixClient extends TypedEventEmitter { return this.sendStateEvent(roomId, EventType.RoomName, { name: name }); } /** - * @param {string} roomId - * @param {string} topic - * @param {string} htmlTopic Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param htmlTopic - Optional. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ public setRoomTopic( roomId: string, @@ -3731,9 +3920,8 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags", { @@ -3744,11 +3932,10 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { @@ -3760,10 +3947,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { @@ -3775,11 +3961,10 @@ export class MatrixClient extends TypedEventEmitter { let content = { - users: {}, + users: {} as Record, }; - if (event?.getType() === EventType.RoomPowerLevels) { + if (event.getType() === EventType.RoomPowerLevels) { // take a copy of the content to ensure we don't corrupt // existing client state with a failed power level change content = utils.deepCopy(event.getContent()); } - content.users[userId] = powerLevel; + if (Array.isArray(userId)) { + for (const user of userId) { + content.users[user] = powerLevel; + } + } else { + content.users[userId] = powerLevel; + } const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", { $roomId: roomId, }); @@ -3826,9 +4013,7 @@ 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 @@ -3914,12 +4098,10 @@ export class MatrixClient extends TypedEventEmitter { let cancelled = false; @@ -4109,9 +4289,9 @@ 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(); @@ -4291,12 +4466,9 @@ export class MatrixClient extends TypedEventEmitter { @@ -4740,11 +4885,8 @@ export class MatrixClient extends TypedEventEmitter { if (this.isGuest()) { @@ -4770,12 +4912,12 @@ export class MatrixClient extends TypedEventEmitter { return this.membershipChange(roomId, userId, "invite", reason); @@ -4855,10 +4995,10 @@ export class MatrixClient extends TypedEventEmitter { return this.inviteByThreePid(roomId, "email", email); @@ -4866,11 +5006,11 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri( @@ -4905,9 +5045,8 @@ export class MatrixClient extends TypedEventEmitter { return this.membershipChange(roomId, undefined, "leave"); @@ -4918,10 +5057,10 @@ export class MatrixClient extends TypedEventEmitter { return this.membershipChange(roomId, userId, "ban", reason); } /** - * @param {string} roomId - * @param {boolean} deleteRoom True to delete the room from the store on success. + * @param deleteRoom - True to delete the room from the store on success. * Default: true. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: `{}` an empty object. + * @returns Rejects: with an error response. */ public forget(roomId: string, deleteRoom = true): Promise<{}> { const promise = this.membershipChange(roomId, undefined, "forget"); @@ -4991,10 +5127,8 @@ export class MatrixClient extends TypedEventEmitter { // unbanning != set their state to leave: this used to be @@ -5012,11 +5146,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/kick", { @@ -5050,10 +5182,10 @@ export class MatrixClient extends TypedEventEmitter; @@ -5080,9 +5212,8 @@ export class MatrixClient extends TypedEventEmitter { const prom = await this.setProfileInfo("displayname", { displayname: name }); @@ -5096,9 +5227,8 @@ export class MatrixClient extends TypedEventEmitter { const prom = await this.setProfileInfo("avatar_url", { avatar_url: url }); @@ -5114,15 +5244,15 @@ export class MatrixClient extends TypedEventEmitterThis method is experimental and * may change. - * @param {string} mxcUrl The MXC URL - * @param {Number} width The desired width of the thumbnail. - * @param {Number} height The desired height of the thumbnail. - * @param {string} resizeMethod The thumbnail resize method to use, either + * @param mxcUrl - The MXC URL + * @param width - The desired width of the thumbnail. + * @param height - The desired height of the thumbnail. + * @param resizeMethod - The thumbnail resize method to use, either * "crop" or "scale". - * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs + * @param allowDirectLinks - If true, return any non-mxc URLs * directly. Fetching such URLs will leak information about the user to * anyone they share a room with. If false, will return null for such URLs. - * @return {?string} the avatar URL or null. + * @returns the avatar URL or null. */ public mxcUrlToHttp( mxcUrl: string, @@ -5135,11 +5265,9 @@ export class MatrixClient extends TypedEventEmitter { @@ -5155,9 +5283,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/presence/$userId/status", { @@ -5175,13 +5303,13 @@ export class MatrixClient extends TypedEventEmitterRoom.oldState.paginationToken will be - * null. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Room. If you are at the beginning + * of the timeline, `Room.oldState.paginationToken` will be + * `null`. + * @returns Rejects: with an error response. */ public scrollback(room: Room, limit = 30): Promise { let timeToWaitMs = 0; @@ -5229,11 +5357,11 @@ export class MatrixClient extends TypedEventEmitter { @@ -5250,13 +5378,6 @@ export class MatrixClient extends TypedEventEmitter> { // don't allow any timeline support unless it's been enabled. @@ -5436,7 +5557,8 @@ export class MatrixClient extends TypedEventEmitter> { // don't allow any timeline support unless it's been enabled. @@ -5574,12 +5697,9 @@ export class MatrixClient extends TypedEventEmitter { @@ -5830,8 +5942,14 @@ export class MatrixClient extends TypedEventEmitter { const mapper = this.getEventMapper(); const matrixEvents = res.chunk.map(mapper); - for (const event of matrixEvents) { - await eventTimeline.getTimelineSet()?.thread?.processEvent(event); + + // Process latest events first + for (const event of matrixEvents.slice().reverse()) { + await thread?.processEvent(event); + const sender = event.getSender()!; + if (!backwards || thread?.getEventReadUpTo(sender) === null) { + room.addLocalEchoReceipt(sender, event, ReceiptType.Read); + } } const newToken = res.next_batch; @@ -5839,7 +5957,8 @@ export class MatrixClient extends TypedEventEmitter { this.peekSync?.stopPeeking(); @@ -5958,16 +6077,10 @@ export class MatrixClient extends TypedEventEmitter { const writePromise = this.sendStateEvent(roomId, EventType.RoomGuestAccess, { @@ -5992,11 +6105,11 @@ export class MatrixClient extends TypedEventEmitter( endpoint: string, @@ -6215,11 +6328,11 @@ export class MatrixClient extends TypedEventEmitter | undefined { + public setRoomMutePushRule(scope: "global" | "device", roomId: string, mute: boolean): Promise | undefined { let promise: Promise | undefined; let hasDontNotifyRule = false; @@ -6327,20 +6440,15 @@ export class MatrixClient extends TypedEventEmitter { // TODO: support search groups @@ -6372,9 +6480,9 @@ export class MatrixClient extends TypedEventEmitter(searchResults: T): Promise { // TODO: we should implement a backoff (as per scrollback()) to deal more @@ -6408,10 +6516,8 @@ export class MatrixClient extends TypedEventEmitter(searchResults: T, response: ISearchResponse): T { @@ -6450,9 +6556,9 @@ export class MatrixClient extends TypedEventEmitter { // Guard against multiple calls whilst ongoing and multiple calls post success @@ -6478,9 +6584,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/filter", { @@ -6497,12 +6603,12 @@ export class MatrixClient extends TypedEventEmitter { if (allowCached) { @@ -6526,9 +6632,7 @@ export class MatrixClient extends TypedEventEmitter} Filter ID + * @returns Filter ID */ public async getOrCreateFilter(filterName: string, filter: Filter): Promise { const filterId = this.store.getFilterIdByName(filterName); @@ -6581,8 +6685,8 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/openid/request_token", { @@ -6601,8 +6705,8 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest(Method.Get, "/voip/turnServer"); @@ -6610,7 +6714,7 @@ export class MatrixClient extends TypedEventEmitter} The servers or an empty list. + * @returns The servers or an empty list. */ public getTurnServers(): ITurnServer[] { return this.turnServers || []; @@ -6619,7 +6723,7 @@ export class MatrixClient extends TypedEventEmitterThis function is implementation specific and may change * as a result. - * @return {boolean} true if the user appears to be a Synapse administrator. + * @returns true if the user appears to be a Synapse administrator. */ public isSynapseAdministrator(): Promise { const path = utils.encodeUri( @@ -6719,8 +6822,8 @@ export class MatrixClient extends TypedEventEmitterThis function is implementation specific and may change as a * result. - * @param {string} userId the User ID to look up. - * @return {object} the whois response - see Synapse docs for information. + * @param userId - the User ID to look up. + * @returns the whois response - see Synapse docs for information. */ public whoisSynapseUser(userId: string): Promise { const path = utils.encodeUri( @@ -6733,8 +6836,8 @@ export class MatrixClient extends TypedEventEmitterThis * function is implementation specific and may change as a result. - * @param {string} userId the User ID to deactivate. - * @return {object} the deactivate response - see Synapse docs for information. + * @param userId - the User ID to deactivate. + * @returns the deactivate response - see Synapse docs for information. */ public deactivateSynapseUser(userId: string): Promise { const path = utils.encodeUri( @@ -6767,8 +6870,8 @@ export class MatrixClient extends TypedEventEmitter { // XXX: Intended private, used in code const primTypes = ["boolean", "string", "number"]; @@ -6776,7 +6879,7 @@ export class MatrixClient extends TypedEventEmitter { return primTypes.includes(typeof value); }) - .reduce((obj, [key, value]) => { + .reduce>((obj, [key, value]) => { obj[key] = value; return obj; }, {}); @@ -6785,9 +6888,9 @@ export class MatrixClient extends TypedEventEmitter} Resolves to a set of rooms - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param userId - The userId to check. + * @returns Promise which resolves to a set of rooms + * @returns Rejects: with an error response. */ public async _unstable_getSharedRooms(userId: string): Promise { // eslint-disable-line const sharedRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666"); @@ -6812,7 +6915,7 @@ export class MatrixClient extends TypedEventEmitter} The server /versions response + * @returns The server /versions response */ public async getVersions(): Promise { if (this.serverVersionsPromise) { @@ -6845,8 +6948,8 @@ export class MatrixClient extends TypedEventEmitter} Whether it is supported + * @param version - The spec version (such as "r0.5.0") to check for. + * @returns Whether it is supported */ public async isVersionSupported(version: string): Promise { const { versions } = await this.getVersions(); @@ -6855,7 +6958,7 @@ export class MatrixClient extends TypedEventEmitter} true if server supports lazy loading + * @returns true if server supports lazy loading */ public async doesServerSupportLazyLoading(): Promise { const response = await this.getVersions(); @@ -6871,7 +6974,7 @@ export class MatrixClient extends TypedEventEmitter} true if id_server parameter is required + * @returns true if id_server parameter is required */ public async doesServerRequireIdServerParam(): Promise { const response = await this.getVersions(); @@ -6897,7 +7000,7 @@ export class MatrixClient extends TypedEventEmitter} true if id_access_token can be sent + * @returns true if id_access_token can be sent */ public async doesServerAcceptIdentityAccessToken(): Promise { const response = await this.getVersions(); @@ -6913,7 +7016,7 @@ export class MatrixClient extends TypedEventEmitter} true if separate functions are supported + * @returns true if separate functions are supported */ public async doesServerSupportSeparateAddAndBind(): Promise { const response = await this.getVersions(); @@ -6928,8 +7031,8 @@ export class MatrixClient extends TypedEventEmitter} true if the feature is supported + * @param feature - the feature name + * @returns true if the feature is supported */ public async doesServerSupportUnstableFeature(feature: string): Promise { const response = await this.getVersions(); @@ -6941,8 +7044,8 @@ export class MatrixClient extends TypedEventEmitter} true if the server is forcing encryption + * @param presetName - The name of the preset to check. + * @returns true if the server is forcing encryption * for the preset. */ public async doesServerForceEncryptionForPreset(presetName: Preset): Promise { @@ -7001,7 +7104,7 @@ export class MatrixClient extends TypedEventEmitter} true if server supports the `logout_devices` parameter + * @returns true if server supports the `logout_devices` parameter */ public doesServerSupportLogoutDevices(): Promise { return this.isVersionSupported("r0.6.1"); @@ -7009,7 +7112,7 @@ export class MatrixClient extends TypedEventEmitter { const fetchedEventType = eventType ? this.getEncryptedIfNeededEventType(roomId, eventType) : null; - const result = await this.fetchRelations( - roomId, - eventId, - relationType, - fetchedEventType, - opts); + const [eventResult, result] = await Promise.all([ + this.fetchRoomEvent(roomId, eventId), + this.fetchRelations(roomId, eventId, relationType, fetchedEventType, opts), + ]); const mapper = this.getEventMapper(); - const originalEvent = result.original_event ? mapper(result.original_event) : undefined; + const originalEvent = eventResult ? mapper(eventResult) : undefined; let events = result.chunk.map(mapper); if (fetchedEventType === EventType.RoomMessageEncrypted) { @@ -7093,7 +7194,6 @@ export class MatrixClient extends TypedEventEmitterThis * method is experimental and may change. - * @return {string} A new client secret + * @returns A new client secret */ public generateClientSecret(): string { return randomString(32); @@ -7111,11 +7211,8 @@ export class MatrixClient extends TypedEventEmitter} A decryption promise - * @param {object} options - * @param {boolean} options.isRetry True if this is a retry (enables more logging) - * @param {boolean} options.emit Emits "event.decrypted" if set to true + * @param event - The event to decrypt + * @returns A decryption promise */ public decryptEventIfNeeded(event: MatrixEvent, options?: IDecryptOptions): Promise { if (event.shouldAttemptDecryption() && this.isCryptoEnabled()) { @@ -7142,7 +7239,7 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest<{ available: true }>( @@ -7221,17 +7318,11 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types - opts = opts || {}; - opts.body = opts.body || {}; - return this.registerRequest(opts.body, "guest"); + public registerGuest({ body }: { body?: any } = {}): Promise { // TODO: Types + return this.registerRequest(body || {}, "guest"); } /** - * @param {Object} data parameters for registration request - * @param {string=} kind type of user to register. may be "guest" - * @return {Promise} Resolves: to the /register response - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param data - parameters for registration request + * @param kind - type of user to register. may be "guest" + * @returns Promise which resolves: to the /register response + * @returns Rejects: with an error response. */ public registerRequest(data: IRegisterRequestParams, kind?: string): Promise { const params: { kind?: string } = {}; @@ -7337,9 +7425,9 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the new token. - * @return {module:http-api.MatrixError} Rejects with an error response. + * @param refreshToken - The refresh token. + * @returns Promise which resolves to the new token. + * @returns Rejects with an error response. */ public refreshToken(refreshToken: string): Promise { return this.http.authedRequest( @@ -7355,18 +7443,16 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the available login flows - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves to the available login flows + * @returns Rejects: with an error response. */ public loginFlows(): Promise { return this.http.request(Method.Get, "/login"); } /** - * @param {string} loginType - * @param {Object} data - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ public login(loginType: string, data: any): Promise { // TODO: Types const loginData = { @@ -7391,10 +7477,8 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types return this.login("m.login.password", { @@ -7404,9 +7488,9 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types return this.login("m.login.saml2", { @@ -7415,22 +7499,22 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types return this.login("m.login.token", { @@ -7468,8 +7552,8 @@ export class MatrixClient extends TypedEventEmitter { if (this.crypto?.backupManager?.getKeyBackupEnabled()) { @@ -7497,11 +7581,11 @@ export class MatrixClient extends TypedEventEmitter { const body: any = {}; @@ -7520,8 +7604,8 @@ export class MatrixClient extends TypedEventEmitter>} Resolves: On success, the token response + * @param auth - Optional. Auth data to supply for User-Interactive auth. + * @returns Promise which resolves: On success, the token response * or UIA auth data. */ public requestLoginToken(auth?: IAuthData): Promise> { @@ -7538,10 +7622,10 @@ export class MatrixClient extends TypedEventEmitter{room_id: {string}} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param options - a list of options to pass to the /createRoom API. + * @returns Promise which resolves: `{room_id: {string}}` + * @returns Rejects: with an error response. */ public async createRoom(options: ICreateRoomOpts): Promise<{ room_id: string }> { // eslint-disable-line camelcase // some valid options include: room_alias_name, visibility, invite @@ -7589,12 +7667,12 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/state", { $roomId: roomId }); @@ -7647,11 +7724,9 @@ export class MatrixClient extends TypedEventEmitter> { const path = utils.encodeUri( @@ -7664,12 +7739,11 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/initialSync", @@ -7782,14 +7846,14 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/joined_rooms", {}); @@ -7827,10 +7891,10 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/joined_members", { @@ -7840,16 +7904,14 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/directory/room/$alias", { @@ -7882,9 +7944,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/directory/room/$alias", { @@ -7896,9 +7958,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId }); @@ -7908,9 +7970,9 @@ export class MatrixClient extends TypedEventEmitter { @@ -7936,9 +7997,8 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/directory/list/room/$roomId", { @@ -7949,12 +8009,11 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/directory/list/room/$roomId", { @@ -7966,14 +8025,13 @@ export class MatrixClient extends TypedEventEmitter { + public searchUserDirectory({ term, limit }: { term: string, limit?: number }): Promise { const body: any = { - search_term: opts.term, + search_term: term, }; - if (opts.limit !== undefined) { - body.limit = opts.limit; + if (limit !== undefined) { + body.limit = limit; } return this.http.authedRequest(Method.Post, "/user_directory/search", undefined, body); @@ -8010,27 +8067,13 @@ export class MatrixClient extends TypedEventEmitterfile.name. - * - * @param {boolean=} opts.includeFilename if false will not send the filename, - * e.g for encrypted file uploads where filename leaks are undesirable. - * Defaults to true. - * - * @param {string=} opts.type Content-type for the upload. Defaults to - * file.type, or applicaton/octet-stream. - * - * @param {Function=} opts.progressHandler Optional. Called when a chunk of - * data has been uploaded, with an object containing the fields `loaded` - * (number of bytes transferred) and `total` (total size, if known). - * - * @return {Promise} Resolves to response object, as + * @returns Promise which resolves to response object, as * determined by this.opts.onlyData, opts.rawResponse, and * opts.onlyContentUri. Rejects with an error (usually a MatrixError). */ @@ -8040,8 +8083,8 @@ export class MatrixClient extends TypedEventEmitter): boolean { return this.http.cancelUpload(upload); @@ -8049,7 +8092,7 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest(Method.Get, "/account/3pid"); @@ -8094,10 +8136,8 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types const path = "/account/3pid"; @@ -8115,10 +8155,10 @@ export class MatrixClient extends TypedEventEmitter/requestToken` on the homeserver. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ public async addThreePidOnly(data: IAddThreePidOnlyBody): Promise<{}> { const path = "/account/3pid/add"; @@ -8134,11 +8174,11 @@ export class MatrixClient extends TypedEventEmitter/requestToken` on the identity server. It should also * contain `id_server` and `id_access_token` fields as well. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ public async bindThreePid(data: IBindThreePidBody): Promise<{}> { const path = "/account/3pid/bind"; @@ -8151,11 +8191,11 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest(Method.Get, "/devices"); @@ -8223,9 +8262,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/devices/$device_id", { @@ -8237,10 +8276,10 @@ export class MatrixClient extends TypedEventEmitter { @@ -8254,10 +8293,10 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/devices/$device_id", { @@ -8276,10 +8315,10 @@ export class MatrixClient extends TypedEventEmitter { const body: any = { devices }; @@ -8295,8 +8334,8 @@ export class MatrixClient extends TypedEventEmitter { const response = await this.http.authedRequest<{ pushers: IPusher[] }>(Method.Get, "/pushers"); @@ -8318,9 +8357,9 @@ export class MatrixClient extends TypedEventEmitter { const path = "/pushers/set"; @@ -8329,10 +8368,8 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest(Method.Get, "/pushrules/").then((rules: IPushRules) => { @@ -8354,12 +8391,8 @@ export class MatrixClient extends TypedEventEmitter { const queryParams: any = {}; - if (opts.next_batch) { - queryParams.next_batch = opts.next_batch; + if (nextBatch) { + queryParams.next_batch = nextBatch; } - return this.http.authedRequest(Method.Post, "/search", queryParams, opts.body, { abortSignal }); + return this.http.authedRequest(Method.Post, "/search", queryParams, body, { abortSignal }); } /** * Upload keys * - * @param {Object} content body of upload request + * @param content - body of upload request * - * @param {Object=} opts this method no longer takes any opts, + * @param opts - this method no longer takes any opts, * used to take opts.device_id but this was not removed from the spec as a redundant parameter * - * @return {Promise} Resolves: result object. Rejects: with - * an error response ({@link module:http-api.MatrixError}). + * @returns Promise which resolves: result object. Rejects: with + * an error response ({@link MatrixError}). */ public uploadKeysRequest( content: IUploadKeysRequest, @@ -8489,22 +8510,20 @@ export class MatrixClient extends TypedEventEmitter { - const content: any = { + public downloadKeysForUsers(userIds: string[], { token }: { token?: string } = {}): Promise { + const content: IQueryKeysRequest = { device_keys: {}, }; - if ('token' in opts) { - content.token = opts.token; + if (token !== undefined) { + content.token = token; } userIds.forEach((u) => { content.device_keys[u] = []; @@ -8516,15 +8535,15 @@ export class MatrixClient extends TypedEventEmitter { const qps = { @@ -8582,15 +8599,14 @@ export class MatrixClient extends TypedEventEmitter} The hashing information for the identity server. + * @param identityAccessToken - The access token for the identity server. + * @returns The hashing information for the identity server. */ public getIdentityHashDetails(identityAccessToken: string): Promise { // TODO: Types return this.http.idServerRequest( @@ -8783,11 +8799,11 @@ export class MatrixClient extends TypedEventEmitter>} addressPairs An array of 2 element arrays. + * @param addressPairs - An array of 2 element arrays. * The first element of each pair is the address, the second is the 3PID medium. - * Eg: ["email@example.org", "email"] - * @param {string} identityAccessToken The access token for the identity server. - * @returns {Promise>} A collection of address mappings to + * Eg: `["email@example.org", "email"]` + * @param identityAccessToken - The access token for the identity server. + * @returns A collection of address mappings to * found MXIDs. Results where no user could be found will not be listed. */ public async identityHashedLookup( @@ -8834,7 +8850,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'])) { @@ -8867,15 +8882,15 @@ export class MatrixClient extends TypedEventEmitter>} query Array of arrays containing + * @param query - Array of arrays containing * [medium, address] - * @param {string} identityAccessToken The `access_token` field of the Identity + * @param identityAccessToken - The `access_token` field of the Identity * Server `/account/register` response (see {@link registerWithIdentityServer}). * - * @return {Promise} Resolves: Lookup results from IS. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Lookup results from IS. + * @returns Rejects: with an error response. */ public async bulkLookupThreePids(query: [string, string][], identityAccessToken: string): Promise { // TODO: Types // Note: we're using the V2 API by calling this function, but our @@ -8948,11 +8963,11 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types return this.http.idServerRequest( @@ -8967,12 +8982,11 @@ export class MatrixClient extends TypedEventEmitter>} contentMap + * @param eventType - type of event to send * content to send. Map from user_id to device_id to content object. - * @param {string=} txnId transaction id. One will be made up if not + * @param txnId - transaction id. One will be made up if not * supplied. - * @return {Promise} Resolves: to an empty object {} + * @returns Promise which resolves: to an empty object `{}` */ public sendToDevice( eventType: string, @@ -8988,7 +9002,7 @@ export class MatrixClient extends TypedEventEmitter { + const targets = Object.keys(contentMap).reduce>((obj, key) => { obj[key] = Object.keys(contentMap[key]); return obj; }, {}); @@ -9002,7 +9016,7 @@ export class MatrixClient extends TypedEventEmitter { return this.toDeviceMessageQueue.queueBatch(batch); @@ -9011,7 +9025,7 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest>( @@ -9028,10 +9042,10 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types const path = utils.encodeUri("/thirdparty/user/$protocol", { @@ -9082,11 +9096,11 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/report/$eventId", { @@ -9100,12 +9114,12 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the created space. + * @param name - The name of the tree space. + * @returns Promise which resolves to the created space. */ public async unstableCreateFileTree(name: string): Promise { const { room_id: roomId } = await this.createRoom({ @@ -9186,8 +9200,8 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias }); @@ -9297,9 +9311,9 @@ export class MatrixClient extends TypedEventEmitter - * This is only fired for live events received via /sync - it is not fired for - * events received over context, search, or pagination APIs. - * - * @event module:client~MatrixClient#"event" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @example - * matrixClient.on("event", function(event){ - * var sender = event.getSender(); - * }); - */ - -/** - * Fires whenever the SDK receives a new to-device event. - * @event module:client~MatrixClient#"toDeviceEvent" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @example - * matrixClient.on("toDeviceEvent", function(event){ - * var sender = event.getSender(); - * }); - */ - -/** - * Fires whenever the SDK's syncing state is updated. The state can be one of: - *
    - * - *
  • PREPARED: The client has synced with the server at least once and is - * ready for methods to be called on it. This will be immediately followed by - * a state of SYNCING. This is the equivalent of "syncComplete" in the - * previous API.
  • - * - *
  • CATCHUP: The client has detected the connection to the server might be - * available again and will now try to do a sync again. As this sync might take - * a long time (depending how long ago was last synced, and general server - * performance) the client is put in this mode so the UI can reflect trying - * to catch up with the server after losing connection.
  • - * - *
  • SYNCING : The client is currently polling for new events from the server. - * This will be called after processing latest events from a sync.
  • - * - *
  • ERROR : The client has had a problem syncing with the server. If this is - * called before PREPARED then there was a problem performing the initial - * sync. If this is called after PREPARED then there was a problem polling - * the server for updates. This may be called multiple times even if the state is - * already ERROR. This is the equivalent of "syncError" in the previous - * API.
  • - * - *
  • RECONNECTING: The sync connection has dropped, but not (yet) in a way that - * should be considered erroneous. - *
  • - * - *
  • STOPPED: The client has stopped syncing with server due to stopClient - * being called. - *
  • - *
- * State transition diagram: - *
- *                                          +---->STOPPED
- *                                          |
- *              +----->PREPARED -------> SYNCING <--+
- *              |                        ^  |  ^    |
- *              |      CATCHUP ----------+  |  |    |
- *              |        ^                  V  |    |
- *   null ------+        |  +------- RECONNECTING   |
- *              |        V  V                       |
- *              +------->ERROR ---------------------+
- *
- * NB: 'null' will never be emitted by this event.
- *
- * 
- * Transitions: - *
    - * - *
  • null -> PREPARED : Occurs when the initial sync is completed - * first time. This involves setting up filters and obtaining push rules. - * - *
  • null -> ERROR : Occurs when the initial sync failed first time. - * - *
  • ERROR -> PREPARED : Occurs when the initial sync succeeds - * after previously failing. - * - *
  • PREPARED -> SYNCING : Occurs immediately after transitioning - * to PREPARED. Starts listening for live updates rather than catching up. - * - *
  • SYNCING -> RECONNECTING : Occurs when the live update fails. - * - *
  • RECONNECTING -> RECONNECTING : Can occur if the update calls - * continue to fail, but the keepalive calls (to /versions) succeed. - * - *
  • RECONNECTING -> ERROR : Occurs when the keepalive call also fails - * - *
  • ERROR -> SYNCING : Occurs when the client has performed a - * live update after having previously failed. - * - *
  • ERROR -> ERROR : Occurs when the client has failed to keepalive - * for a second time or more.
  • - * - *
  • SYNCING -> SYNCING : Occurs when the client has performed a live - * update. This is called after processing.
  • - * - *
  • * -> STOPPED : Occurs once the client has stopped syncing or - * trying to sync after stopClient has been called.
  • - *
- * - * @event module:client~MatrixClient#"sync" - * - * @param {string} state An enum representing the syncing state. One of "PREPARED", - * "SYNCING", "ERROR", "STOPPED". - * - * @param {?string} prevState An enum representing the previous syncing state. - * One of "PREPARED", "SYNCING", "ERROR", "STOPPED" or null. - * - * @param {?Object} data Data about this transition. - * - * @param {MatrixError} data.error The matrix error if state=ERROR. - * - * @param {String} data.oldSyncToken The 'since' token passed to /sync. - * null for the first successful sync since this client was - * started. Only present if state=PREPARED or - * state=SYNCING. - * - * @param {String} data.nextSyncToken The 'next_batch' result from /sync, which - * will become the 'since' token for the next call to /sync. Only present if - * state=PREPARED or state=SYNCING. - * - * @param {boolean} data.catchingUp True if we are working our way through a - * backlog of events after connecting. Only present if state=SYNCING. - * - * @example - * matrixClient.on("sync", function(state, prevState, data) { - * switch (state) { - * case "ERROR": - * // update UI to say "Connection Lost" - * break; - * case "SYNCING": - * // update UI to remove any "Connection Lost" message - * break; - * case "PREPARED": - * // the client instance is ready to be queried. - * var rooms = matrixClient.getRooms(); - * break; - * } - * }); - */ - -/** - * Fires whenever a new Room is added. This will fire when you are invited to a - * room, as well as when you join a room. This event is experimental and - * may change. - * @event module:client~MatrixClient#"Room" - * @param {Room} room The newly created, fully populated room. - * @example - * matrixClient.on("Room", function(room){ - * var roomId = room.roomId; - * }); - */ - -/** - * Fires whenever a Room is removed. This will fire when you forget a room. - * This event is experimental and may change. - * @event module:client~MatrixClient#"deleteRoom" - * @param {string} roomId The deleted room ID. - * @example - * matrixClient.on("deleteRoom", function(roomId){ - * // update UI from getRooms() - * }); - */ - -/** - * Fires whenever an incoming call arrives. - * @event module:client~MatrixClient#"Call.incoming" - * @param {module:webrtc/call~MatrixCall} call The incoming call. - * @example - * matrixClient.on("Call.incoming", function(call){ - * call.answer(); // auto-answer - * }); - */ - -/** - * Fires whenever the login session the JS SDK is using is no - * longer valid and the user must log in again. - * NB. This only fires when action is required from the user, not - * when then login session can be renewed by using a refresh token. - * @event module:client~MatrixClient#"Session.logged_out" - * @example - * matrixClient.on("Session.logged_out", function(errorObj){ - * // show the login screen - * }); - */ - -/** - * Fires when the JS SDK receives a M_CONSENT_NOT_GIVEN error in response - * to a HTTP request. - * @event module:client~MatrixClient#"no_consent" - * @example - * matrixClient.on("no_consent", function(message, contentUri) { - * console.info(message + ' Go to ' + contentUri); - * }); - */ - -/** - * Fires when a device is marked as verified/unverified/blocked/unblocked by - * {@link module:client~MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or - * {@link module:client~MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}. - * - * @event module:client~MatrixClient#"deviceVerificationChanged" - * @param {string} userId the owner of the verified device - * @param {string} deviceId the id of the verified device - * @param {module:crypto/deviceinfo} deviceInfo updated device information - */ - -/** - * Fires when the trust status of a user changes - * If userId is the userId of the logged in user, this indicated a change - * in the trust status of the cross-signing data on the account. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @event module:client~MatrixClient#"userTrustStatusChanged" - * @param {string} userId the userId of the user in question - * @param {UserTrustLevel} trustLevel The new trust level of the user - */ - -/** - * Fires when the user's cross-signing keys have changed or cross-signing - * has been enabled/disabled. The client can use getStoredCrossSigningForUser - * with the user ID of the logged in user to check if cross-signing is - * enabled on the account. If enabled, it can test whether the current key - * is trusted using with checkUserTrust with the user ID of the logged - * in user. The checkOwnCrossSigningTrust function may be used to reconcile - * the trust in the account key. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @event module:client~MatrixClient#"crossSigning.keysChanged" - */ - -/** - * Fires whenever new user-scoped account_data is added. - * @event module:client~MatrixClient#"accountData" - * @param {MatrixEvent} event The event describing the account_data just added - * @param {MatrixEvent} event The previous account data, if known. - * @example - * matrixClient.on("accountData", function(event, oldEvent){ - * myAccountData[event.type] = event.content; - * }); - */ - -/** - * Fires whenever the stored devices for a user have changed - * @event module:client~MatrixClient#"crypto.devicesUpdated" - * @param {String[]} users A list of user IDs that were updated - * @param {boolean} initialFetch If true, the store was empty (apart - * from our own device) and has been seeded. - */ - -/** - * Fires whenever the stored devices for a user will be updated - * @event module:client~MatrixClient#"crypto.willUpdateDevices" - * @param {String[]} users A list of user IDs that will be updated - * @param {boolean} initialFetch If true, the store is empty (apart - * from our own device) and is being seeded. - */ - -/** - * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() - * @event module:client~MatrixClient#"crypto.keyBackupStatus" - * @param {boolean} enabled true if key backup has been enabled, otherwise false - * @example - * matrixClient.on("crypto.keyBackupStatus", function(enabled){ - * if (enabled) { - * [...] - * } - * }); - */ - -/** - * Fires when we want to suggest to the user that they restore their megolm keys - * from backup or by cross-signing the device. - * - * @event module:client~MatrixClient#"crypto.suggestKeyRestore" - */ - -/** - * Fires when a key verification is requested. - * @event module:client~MatrixClient#"crypto.verification.request" - * @param {object} data - * @param {MatrixEvent} data.event the original verification request message - * @param {Array} data.methods the verification methods that can be used - * @param {Number} data.timeout the amount of milliseconds that should be waited - * before cancelling the request automatically. - * @param {Function} data.beginKeyVerification a function to call if a key - * verification should be performed. The function takes one argument: the - * name of the key verification method (taken from data.methods) to use. - * @param {Function} data.cancel a function to call if the key verification is - * rejected. - */ - -/** - * Fires when a key verification is requested with an unknown method. - * @event module:client~MatrixClient#"crypto.verification.request.unknown" - * @param {string} userId the user ID who requested the key verification - * @param {Function} cancel a function that will send a cancellation message to - * reject the key verification. - */ - -/** - * Fires when a secret request has been cancelled. If the client is prompting - * the user to ask whether they want to share a secret, the prompt can be - * dismissed. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @event module:client~MatrixClient#"crypto.secrets.requestCancelled" - * @param {object} data - * @param {string} data.user_id The user ID of the client that had requested the secret. - * @param {string} data.device_id The device ID of the client that had requested the - * secret. - * @param {string} data.request_id The ID of the original request. - */ - -/** - * Fires when the client .well-known info is fetched. - * - * @event module:client~MatrixClient#"WellKnown.client" - * @param {object} data The JSON object returned by the server - */ diff --git a/src/content-helpers.ts b/src/content-helpers.ts index 6fa4b684b..b66f0d102 100644 --- a/src/content-helpers.ts +++ b/src/content-helpers.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** @module ContentHelpers */ - import { isProvided, REFERENCE_RELATION } from "matrix-events-sdk"; import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon"; @@ -37,9 +35,9 @@ import { IContent } from "./models/event"; /** * Generates the content for a HTML Message event - * @param {string} body the plaintext body of the message - * @param {string} htmlBody the HTML representation of the message - * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + * @param body - the plaintext body of the message + * @param htmlBody - the HTML representation of the message + * @returns */ export function makeHtmlMessage(body: string, htmlBody: string): IContent { return { @@ -52,9 +50,9 @@ export function makeHtmlMessage(body: string, htmlBody: string): IContent { /** * Generates the content for a HTML Notice event - * @param {string} body the plaintext body of the notice - * @param {string} htmlBody the HTML representation of the notice - * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + * @param body - the plaintext body of the notice + * @param htmlBody - the HTML representation of the notice + * @returns */ export function makeHtmlNotice(body: string, htmlBody: string): IContent { return { @@ -67,9 +65,9 @@ export function makeHtmlNotice(body: string, htmlBody: string): IContent { /** * Generates the content for a HTML Emote event - * @param {string} body the plaintext body of the emote - * @param {string} htmlBody the HTML representation of the emote - * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + * @param body - the plaintext body of the emote + * @param htmlBody - the HTML representation of the emote + * @returns */ export function makeHtmlEmote(body: string, htmlBody: string): IContent { return { @@ -82,8 +80,8 @@ export function makeHtmlEmote(body: string, htmlBody: string): IContent { /** * Generates the content for a Plaintext Message event - * @param {string} body the plaintext body of the emote - * @returns {{msgtype: string, body: string}} + * @param body - the plaintext body of the emote + * @returns */ export function makeTextMessage(body: string): IContent { return { @@ -94,8 +92,8 @@ export function makeTextMessage(body: string): IContent { /** * Generates the content for a Plaintext Notice event - * @param {string} body the plaintext body of the notice - * @returns {{msgtype: string, body: string}} + * @param body - the plaintext body of the notice + * @returns */ export function makeNotice(body: string): IContent { return { @@ -106,8 +104,8 @@ export function makeNotice(body: string): IContent { /** * Generates the content for a Plaintext Emote event - * @param {string} body the plaintext body of the emote - * @returns {{msgtype: string, body: string}} + * @param body - the plaintext body of the emote + * @returns */ export function makeEmoteMessage(body: string): IContent { return { @@ -139,11 +137,11 @@ export const getTextForLocationEvent = ( /** * Generates the content for a Location event - * @param uri a geo:// uri for the location - * @param timestamp the timestamp when the location was correct (milliseconds since the UNIX epoch) - * @param description the (optional) label for this location on the map - * @param assetType the (optional) asset type of this location e.g. "m.self" - * @param text optional. A text for the location + * @param uri - a geo:// uri for the location + * @param timestamp - the timestamp when the location was correct (milliseconds since the UNIX epoch) + * @param description - the (optional) label for this location on the map + * @param assetType - the (optional) asset type of this location e.g. "m.self" + * @param text - optional. A text for the location */ export const makeLocationContent = ( // this is first but optional diff --git a/src/content-repo.ts b/src/content-repo.ts index d6cf81f2e..9b6f6d372 100644 --- a/src/content-repo.ts +++ b/src/content-repo.ts @@ -13,25 +13,22 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module content-repo - */ import * as utils from "./utils"; /** * Get the HTTP URL for an MXC URI. - * @param {string} baseUrl The base homeserver url which has a content repo. - * @param {string} mxc The mxc:// URI. - * @param {Number} width The desired width of the thumbnail. - * @param {Number} height The desired height of the thumbnail. - * @param {string} resizeMethod The thumbnail resize method to use, either + * @param baseUrl - The base homeserver url which has a content repo. + * @param mxc - The mxc:// URI. + * @param width - The desired width of the thumbnail. + * @param height - The desired height of the thumbnail. + * @param resizeMethod - The thumbnail resize method to use, either * "crop" or "scale". - * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs + * @param allowDirectLinks - If true, return any non-mxc URLs * directly. Fetching such URLs will leak information about the user to * anyone they share a room with. If false, will return the emptry string * for such URLs. - * @return {string} The complete URL to the content. + * @returns The complete URL to the content. */ export function getHttpUriForMxc( baseUrl: string, diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index e776b93ad..1ac9e2144 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -16,7 +16,6 @@ limitations under the License. /** * Cross signing methods - * @module crypto/CrossSigning */ import { PkSigning } from "@matrix-org/olm"; @@ -67,12 +66,10 @@ export class CrossSigningInfo { /** * Information about a user's cross-signing keys * - * @class - * - * @param {string} userId the user that the information is about - * @param {object} callbacks Callbacks used to interact with the app + * @param userId - the user that the information is about + * @param callbacks - Callbacks used to interact with the app * Requires getCrossSigningKey and saveCrossSigningKeys - * @param {object} cacheCallbacks Callbacks used to interact with the cache + * @param cacheCallbacks - Callbacks used to interact with the cache */ public constructor( public readonly userId: string, @@ -84,6 +81,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]; } } @@ -101,10 +99,10 @@ export class CrossSigningInfo { /** * Calls the app callback to ask for a private key * - * @param {string} type The key type ("master", "self_signing", or "user_signing") - * @param {string} expectedPubkey The matching public key or undefined to use + * @param type - The key type ("master", "self_signing", or "user_signing") + * @param expectedPubkey - The matching public key or undefined to use * the stored public key for the given key type. - * @returns {Array} An array with [ public key, Olm.PkSigning ] + * @returns An array with [ public key, Olm.PkSigning ] */ public async getCrossSigningKey(type: string, expectedPubkey?: string): Promise<[string, PkSigning]> { const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0; @@ -164,8 +162,8 @@ export class CrossSigningInfo { * XXX: This could be static, be we often seem to have an instance when we * want to know this anyway... * - * @param {SecretStorage} secretStorage The secret store using account data - * @returns {object} map of key name to key info the secret is encrypted + * @param secretStorage - The secret store using account data + * @returns map of key name to key info the secret is encrypted * with, or null if it is not present or not encrypted with a trusted * key */ @@ -193,8 +191,8 @@ export class CrossSigningInfo { * typically called in conjunction with the creation of new cross-signing * keys. * - * @param {Map} keys The keys to store - * @param {SecretStorage} secretStorage The secret store using account data + * @param keys - The keys to store + * @param secretStorage - The secret store using account data */ public static async storeInSecretStorage( keys: Map, @@ -210,10 +208,10 @@ export class CrossSigningInfo { * Get private keys from secret storage created by some other device. This * also passes the private keys to the app-specific callback. * - * @param {string} type The type of key to get. One of "master", + * @param type - The type of key to get. One of "master", * "self_signing", or "user_signing". - * @param {SecretStorage} secretStorage The secret store using account data - * @return {Uint8Array} The private key + * @param secretStorage - The secret store using account data + * @returns The private key */ public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise { const encodedKey = await secretStorage.get(`m.cross_signing.${type}`); @@ -226,9 +224,9 @@ export class CrossSigningInfo { /** * Check whether the private keys exist in the local key cache. * - * @param {string} [type] The type of key to get. One of "master", + * @param type - The type of key to get. One of "master", * "self_signing", or "user_signing". Optional, will check all by default. - * @returns {boolean} True if all keys are stored in the local cache. + * @returns True if all keys are stored in the local cache. */ public async isStoredInKeyCache(type?: string): Promise { const cacheCallbacks = this.cacheCallbacks; @@ -245,7 +243,7 @@ export class CrossSigningInfo { /** * Get cross-signing private keys from the local cache. * - * @returns {Map} A map from key type (string) to private key (Uint8Array) + * @returns A map from key type (string) to private key (Uint8Array) */ public async getCrossSigningKeysFromCache(): Promise> { const keys = new Map(); @@ -265,10 +263,10 @@ export class CrossSigningInfo { * Get the ID used to identify the user. This can also be used to test for * the existence of a given key type. * - * @param {string} type The type of key to get the ID of. One of "master", + * @param type - The type of key to get the ID of. One of "master", * "self_signing", or "user_signing". Defaults to "master". * - * @return {string} the ID + * @returns the ID */ public getId(type = "master"): string | null { if (!this.keys[type]) return null; @@ -281,7 +279,7 @@ export class CrossSigningInfo { * will be held in this class, while the private keys are passed off to the * `saveCrossSigningKeys` application callback. * - * @param {CrossSigningLevel} level The key types to reset + * @param level - The key types to reset */ public async resetKeys(level?: CrossSigningLevel): Promise { if (!this.callbacks.saveCrossSigningKeys) { @@ -501,9 +499,9 @@ export class CrossSigningInfo { /** * Check whether a given user is trusted. * - * @param {CrossSigningInfo} userCrossSigning Cross signing info for user + * @param userCrossSigning - Cross signing info for user * - * @returns {UserTrustLevel} + * @returns */ public checkUserTrust(userCrossSigning: CrossSigningInfo): UserTrustLevel { // if we're checking our own key, then it's trusted if the master key @@ -541,12 +539,12 @@ export class CrossSigningInfo { /** * Check whether a given device is trusted. * - * @param {CrossSigningInfo} userCrossSigning Cross signing info for user - * @param {module:crypto/deviceinfo} device The device to check - * @param {boolean} localTrust Whether the device is trusted locally - * @param {boolean} trustCrossSignedDevices Whether we trust cross signed devices + * @param userCrossSigning - Cross signing info for user + * @param device - The device to check + * @param localTrust - Whether the device is trusted locally + * @param trustCrossSignedDevices - Whether we trust cross signed devices * - * @returns {DeviceTrustLevel} + * @returns */ public checkDeviceTrust( userCrossSigning: CrossSigningInfo, @@ -579,7 +577,7 @@ export class CrossSigningInfo { } /** - * @returns {object} Cache callbacks + * @returns Cache callbacks */ public getCacheCallbacks(): ICacheCallbacks { return this.cacheCallbacks; @@ -620,21 +618,21 @@ export class UserTrustLevel { ) {} /** - * @returns {boolean} true if this user is verified via any means + * @returns true if this user is verified via any means */ public isVerified(): boolean { return this.isCrossSigningVerified(); } /** - * @returns {boolean} true if this user is verified via cross signing + * @returns true if this user is verified via cross signing */ public isCrossSigningVerified(): boolean { return this.crossSigningVerified; } /** - * @returns {boolean} true if we ever verified this user before (at least for + * @returns true if we ever verified this user before (at least for * the history of verifications observed by this device). */ public wasCrossSigningVerified(): boolean { @@ -642,7 +640,7 @@ export class UserTrustLevel { } /** - * @returns {boolean} true if this user's key is trusted on first use + * @returns true if this user's key is trusted on first use */ public isTofu(): boolean { return this.tofu; @@ -674,7 +672,7 @@ export class DeviceTrustLevel { } /** - * @returns {boolean} true if this device is verified via any means + * @returns true if this device is verified via any means */ public isVerified(): boolean { return Boolean(this.isLocallyVerified() || ( @@ -683,21 +681,21 @@ export class DeviceTrustLevel { } /** - * @returns {boolean} true if this device is verified via cross signing + * @returns true if this device is verified via cross signing */ public isCrossSigningVerified(): boolean { return this.crossSigningVerified; } /** - * @returns {boolean} true if this device is verified locally + * @returns true if this device is verified locally */ public isLocallyVerified(): boolean { return this.localVerified; } /** - * @returns {boolean} true if this device is trusted from a user's key + * @returns true if this device is trusted from a user's key * that is trusted on first use */ public isTofu(): boolean { @@ -756,9 +754,9 @@ export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning], /** * Request cross-signing keys from another device during verification. * - * @param {MatrixClient} baseApis base Matrix API interface - * @param {string} userId The user ID being verified - * @param {string} deviceId The device ID being verified + * @param baseApis - base Matrix API interface + * @param userId - The user ID being verified + * @param deviceId - The device ID being verified */ export async function requestKeysDuringVerification( baseApis: MatrixClient, diff --git a/src/crypto/DeviceList.ts b/src/crypto/DeviceList.ts index da40a03f2..81dc5e18e 100644 --- a/src/crypto/DeviceList.ts +++ b/src/crypto/DeviceList.ts @@ -15,8 +15,6 @@ limitations under the License. */ /** - * @module crypto/DeviceList - * * Manages the list of other users' devices */ @@ -64,9 +62,6 @@ export type DeviceInfoMap = Record>; type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated; -/** - * @alias module:crypto/DeviceList - */ export class DeviceList extends TypedEventEmitter { private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {}; @@ -163,11 +158,11 @@ export class DeviceList extends TypedEventEmitter} true if the data was saved, false if + * @returns true if the data was saved, false if * it was not (eg. because no changes were pending). The promise * will only resolve once the data is saved, so may take some time * to resolve. @@ -236,7 +231,7 @@ export class DeviceList extends TypedEventEmitterdeviceId->{@link - * module:crypto/deviceinfo|DeviceInfo}. + * @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo}. */ public downloadKeys(userIds: string[], forceDownload: boolean): Promise { const usersToDownload: string[] = []; @@ -303,9 +297,9 @@ export class DeviceList extends TypedEventEmitterdeviceId->{@link module:crypto/deviceinfo|DeviceInfo}. + * @returns userId-\>deviceId-\>{@link DeviceInfo}. */ private getDevicesFromStore(userIds: string[]): DeviceInfoMap { const stored: DeviceInfoMap = {}; @@ -322,7 +316,7 @@ export class DeviceList extends TypedEventEmitter{object} devices, or undefined if + * @returns `deviceId->{object}` devices, or undefined if * there is no data for this user. */ public getRawStoredDevicesForUser(userId: string): Record { @@ -376,10 +370,8 @@ export class DeviceList extends TypedEventEmitter): void { this.setRawStoredDevicesForUser(userId, devices); @@ -468,7 +458,6 @@ export class DeviceList extends TypedEventEmitter { @@ -569,9 +556,9 @@ export class DeviceList extends TypedEventEmitter{object} the new devices + * @param devices - `deviceId->{object}` the new devices */ public setRawStoredDevicesForUser(userId: string, devices: Record): void { // remove old devices from userByIdentityKey @@ -602,9 +589,9 @@ export class DeviceList extends TypedEventEmitter; + keys: Record<"master" | "self_signing" | "user_signing", ICrossSigningKey>; } /** @@ -58,8 +58,8 @@ export class EncryptionSetupBuilder { private sessionBackupPrivateKey?: Uint8Array; /** - * @param {Object.} accountData pre-existing account data, will only be read, not written. - * @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet + * @param accountData - pre-existing account data, will only be read, not written. + * @param delegateCryptoCallbacks - crypto callbacks to delegate to if the key isn't in cache yet */ public constructor(accountData: Record, delegateCryptoCallbacks?: ICryptoCallbacks) { this.accountDataClientAdapter = new AccountDataClientAdapter(accountData); @@ -70,13 +70,13 @@ export class EncryptionSetupBuilder { /** * Adds new cross-signing public keys * - * @param {function} authUpload Function called to await an interactive auth + * @param authUpload - Function called to await an interactive auth * flow when uploading device signing keys. * Args: - * {function} A function that makes the request requiring auth. Receives + * A function that makes the request requiring auth. Receives * the auth data as an object. Can be called multiple times, first with * an empty authDict, to obtain the flows. - * @param {Object} keys the new keys + * @param keys - the new keys */ public addCrossSigningKeys(authUpload: ICrossSigningKeys["authUpload"], keys: ICrossSigningKeys["keys"]): void { this.crossSigningKeys = { authUpload, keys }; @@ -88,7 +88,7 @@ export class EncryptionSetupBuilder { * Used either to create a new key backup, or add signatures * from the new MSK. * - * @param {Object} keyBackupInfo as received from/sent to the server + * @param keyBackupInfo - as received from/sent to the server */ public addSessionBackup(keyBackupInfo: IKeyBackupInfo): void { this.keyBackupInfo = keyBackupInfo; @@ -99,7 +99,6 @@ export class EncryptionSetupBuilder { * * Used after fixing the format of the key * - * @param {Uint8Array} privateKey */ public addSessionBackupPrivateKeyToCache(privateKey: Uint8Array): void { this.sessionBackupPrivateKey = privateKey; @@ -109,9 +108,6 @@ export class EncryptionSetupBuilder { * Add signatures from a given user and device/x-sign key * Used to sign the new cross-signing key with the device key * - * @param {String} userId - * @param {String} deviceId - * @param {Object} signature */ public addKeySignature(userId: string, deviceId: string, signature: ISignedKey): void { if (!this.keySignatures) { @@ -122,18 +118,12 @@ export class EncryptionSetupBuilder { userSignatures[deviceId] = signature; } - /** - * @param {String} type - * @param {Object} content - * @return {Promise} - */ public async setAccountData(type: string, content: object): Promise { await this.accountDataClientAdapter.setAccountData(type, content); } /** * builds the operation containing all the parts that have been added to the builder - * @return {EncryptionSetupOperation} */ public buildOperation(): EncryptionSetupOperation { const accountData = this.accountDataClientAdapter.values; @@ -150,9 +140,6 @@ export class EncryptionSetupBuilder { * * This does not yet store the operation in a way that it can be restored, * but that is the idea in the future. - * - * @param {Crypto} crypto - * @return {Promise} */ public async persist(crypto: Crypto): Promise { // store private keys in cache @@ -187,10 +174,6 @@ export class EncryptionSetupBuilder { */ export class EncryptionSetupOperation { /** - * @param {Map} accountData - * @param {Object} crossSigningKeys - * @param {Object} keyBackupInfo - * @param {Object} keySignatures */ public constructor( private readonly accountData: Map, @@ -201,7 +184,6 @@ export class EncryptionSetupOperation { /** * Runs the (remaining part of, in the future) operation by sending requests to the server. - * @param {Crypto} crypto */ public async apply(crypto: Crypto): Promise { const baseApis = crypto.baseApis; @@ -209,7 +191,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 @@ -270,23 +252,21 @@ class AccountDataClientAdapter public readonly values = new Map(); /** - * @param {Object.} existingValues existing account data + * @param existingValues - existing account data */ public constructor(private readonly existingValues: Record) { super(); } /** - * @param {String} type - * @return {Promise} the content of the account data + * @returns the content of the account data */ public getAccountDataFromServer(type: string): Promise { return Promise.resolve(this.getAccountData(type) as T); } /** - * @param {String} type - * @return {Object} the content of the account data + * @returns the content of the account data */ public getAccountData(type: string): IContent | null { const modifiedValue = this.values.get(type); @@ -300,11 +280,6 @@ class AccountDataClientAdapter return null; } - /** - * @param {String} type - * @param {Object} content - * @return {Promise} - */ public setAccountData(type: string, content: any): Promise<{}> { const lastEvent = this.values.get(type); this.values.set(type, content); diff --git a/src/crypto/OlmDevice.ts b/src/crypto/OlmDevice.ts index efa34ad41..d7d62099d 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,55 +48,27 @@ 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.`); } } -/** - * The type of object we use for importing and exporting megolm session data. - * - * @typedef {Object} module:crypto/OlmDevice.MegolmSessionData - * @property {String} sender_key Sender's Curve25519 device key - * @property {String[]} forwarding_curve25519_key_chain Devices which forwarded - * this session to us (normally empty). - * @property {Object} sender_claimed_keys Other keys the sender claims. - * @property {String} room_id Room this session is used in - * @property {String} session_id Unique id for the session - * @property {String} session_key Base64'ed key data - */ - interface IInitOpts { fromExportedDevice?: IExportedDevice; pickleKey?: string; } -/** - * data stored in the session store about an inbound group session - * - * @typedef {Object} InboundGroupSessionData - * @property {string} room_id - * @property {string} session pickled Olm.InboundGroupSession - * @property {Object} keysClaimed - * @property {Array} forwardingCurve25519KeyChain Devices involved in forwarding - * this session to us (normally empty). - * @property {boolean=} untrusted whether this session is untrusted. - * @property {boolean=} sharedHistory whether this session exists during the room being set to shared history. - */ - +/** data stored in the session store about an inbound group session */ export interface InboundGroupSessionData { room_id: string; // eslint-disable-line camelcase + /** pickled Olm.InboundGroupSession */ session: string; keysClaimed: Record; + /** Devices involved in forwarding this session to us (normally empty). */ forwardingCurve25519KeyChain: string[]; + /** whether this session is untrusted. */ untrusted?: boolean; + /** whether this session exists during the room being set to shared history. */ sharedHistory?: boolean; } @@ -126,25 +106,20 @@ 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. * * Accounts and sessions are kept pickled in the cryptoStore. - * - * @constructor - * @alias module:crypto/OlmDevice - * - * @param {Object} cryptoStore A store for crypto data - * - * @property {string} deviceCurve25519Key Curve25519 key for the account - * @property {string} deviceEd25519Key Ed25519 key for the account */ export class OlmDevice { public pickleKey = "DEFAULT_KEY"; // set by consumers - // don't know these until we load the account from storage in init() + /** Curve25519 key for the account, unknown until we load the account from storage in init() */ public deviceCurve25519Key: string | null = null; + /** Ed25519 key for the account, unknown until we load the account from storage in init() */ public deviceEd25519Key: string | null = null; private maxOneTimeKeys: number | null = null; @@ -181,7 +156,7 @@ export class OlmDevice { } /** - * @return {array} The version of Olm. + * @returns The version of Olm. */ public static getOlmVersion(): [number, number, number] { return global.Olm.get_library_version(); @@ -199,12 +174,11 @@ export class OlmDevice { * * Reads the device keys from the OlmAccount object. * - * @param {object} opts - * @param {object} opts.fromExportedDevice (Optional) data from exported device + * @param fromExportedDevice - (Optional) data from exported device * that must be re-created. * If present, opts.pickleKey is ignored * (exported data already provides a pickle key) - * @param {object} opts.pickleKey (Optional) pickle key to set instead of default one + * @param pickleKey - (Optional) pickle key to set instead of default one */ public async init({ pickleKey, fromExportedDevice }: IInitOpts = {}): Promise { let e2eKeys; @@ -242,9 +216,9 @@ export class OlmDevice { * Note that for now only the “account” and “sessions” stores are populated; * Other stores will be as with a new device. * - * @param {IExportedDevice} exportedData Data exported from another device + * @param exportedData - Data exported from another device * through the “export” method. - * @param {Olm.Account} account an olm account to initialize + * @param account - an olm account to initialize */ private async initialiseFromExportedDevice(exportedData: IExportedDevice, account: Account): Promise { await this.cryptoStore.doTxn( @@ -302,9 +276,8 @@ export class OlmDevice { * This function requires a live transaction object from cryptoStore.doTxn() * and therefore may only be called in a doTxn() callback. * - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {function} func - * @private + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @internal */ private getAccount(txn: unknown, func: (account: Account) => void): void { this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => { @@ -323,9 +296,9 @@ export class OlmDevice { * This function requires a live transaction object from cryptoStore.doTxn() * and therefore may only be called in a doTxn() callback. * - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {object} Olm.Account object - * @private + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @param Olm.Account object + * @internal */ private storeAccount(txn: unknown, account: Account): void { this.cryptoStore.storeAccount(txn, account.pickle(this.pickleKey)); @@ -335,7 +308,7 @@ export class OlmDevice { * Export data for re-creating the Olm device later. * TODO export data other than just account and (P2P) sessions. * - * @return {Promise} The exported data + * @returns The exported data */ public async export(): Promise { const result: Partial = { @@ -370,11 +343,8 @@ export class OlmDevice { * function and will be freed as soon the callback returns. It is *not* * usable for the rest of the lifetime of the transaction. * - * @param {string} deviceKey - * @param {string} sessionId - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {function} func - * @private + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @internal */ private getSession( deviceKey: string, @@ -394,9 +364,7 @@ export class OlmDevice { * function with it. The session object is destroyed once the function * returns. * - * @param {object} sessionInfo - * @param {function} func - * @private + * @internal */ private unpickleSession( sessionInfo: ISessionInfo, @@ -416,10 +384,9 @@ export class OlmDevice { /** * store our OlmSession in the session store * - * @param {string} deviceKey - * @param {object} sessionInfo {session: OlmSession, lastReceivedMessageTs: int} - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @private + * @param sessionInfo - `{session: OlmSession, lastReceivedMessageTs: int}` + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @internal */ private saveSession(deviceKey: string, sessionInfo: IUnpickledSessionInfo, txn: unknown): void { const sessionId = sessionInfo.session.session_id(); @@ -432,9 +399,8 @@ export class OlmDevice { /** * get an OlmUtility and call the given function * - * @param {function} func - * @return {object} result of func - * @private + * @returns result of func + * @internal */ private getUtility(func: (utility: Utility) => T): T { const utility = new global.Olm.Utility(); @@ -448,11 +414,11 @@ export class OlmDevice { /** * Signs a message with the ed25519 key for this account. * - * @param {string} message message to be signed - * @return {Promise} base64-encoded signature + * @param message - message to be signed + * @returns base64-encoded signature */ public async sign(message: string): Promise { - let result; + let result: string; await this.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { @@ -460,18 +426,18 @@ export class OlmDevice { result = account.sign(message); }); }); - return result; + return result!; } /** * Get the current (unused, unpublished) one-time keys for this account. * - * @return {object} one time keys; an object with the single property + * @returns one time keys; an object with the single property * 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,13 +447,13 @@ export class OlmDevice { }, ); - return result; + return result!; } /** * Get the maximum number of one-time keys we can store. * - * @return {number} number of keys + * @returns number of keys */ public maxNumberOfOneTimeKeys(): number { return this.maxOneTimeKeys ?? -1; @@ -511,8 +477,8 @@ export class OlmDevice { /** * Generate some new one-time keys * - * @param {number} numKeys number of keys to generate - * @return {Promise} Resolved once the account is saved back having generated the keys + * @param numKeys - number of keys to generate + * @returns Resolved once the account is saved back having generated the keys */ public generateOneTimeKeys(numKeys: number): Promise { return this.cryptoStore.doTxn( @@ -529,7 +495,7 @@ export class OlmDevice { /** * Generate a new fallback keys * - * @return {Promise} Resolved once the account is saved back having generated the key + * @returns Resolved once the account is saved back having generated the key */ public async generateFallbackKey(): Promise { await this.cryptoStore.doTxn( @@ -573,9 +539,9 @@ export class OlmDevice { * * The new session will be stored in the cryptoStore. * - * @param {string} theirIdentityKey remote user's Curve25519 identity key - * @param {string} theirOneTimeKey remote user's one-time Curve25519 key - * @return {string} sessionId for the outbound session. + * @param theirIdentityKey - remote user's Curve25519 identity key + * @param theirOneTimeKey - remote user's one-time Curve25519 key + * @returns sessionId for the outbound session. */ public async createOutboundSession(theirIdentityKey: string, theirOneTimeKey: string): Promise { let newSessionId: string; @@ -612,15 +578,14 @@ export class OlmDevice { /** * Generate a new inbound session, given an incoming message * - * @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key - * @param {number} messageType messageType field from the received message (must be 0) - * @param {string} ciphertext base64-encoded body from the received message + * @param theirDeviceIdentityKey - remote user's Curve25519 identity key + * @param messageType - messageType field from the received message (must be 0) + * @param ciphertext - base64-encoded body from the received message * - * @return {{payload: string, session_id: string}} decrypted payload, and + * @returns decrypted payload, and * session id of new session * - * @raises {Error} if the received message was not valid (for instance, it - * didn't use a valid one-time key). + * @throws Error if the received message was not valid (for instance, it didn't use a valid one-time key). */ public async createInboundSession( theirDeviceIdentityKey: string, @@ -673,9 +638,9 @@ export class OlmDevice { /** * Get a list of known session IDs for the given device * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @return {Promise} a list of known session ids for the device + * @returns a list of known session ids for the device */ public async getSessionIdsForDevice(theirDeviceIdentityKey: string): Promise { const log = logger.withPrefix("[getSessionIdsForDevice]"); @@ -708,13 +673,13 @@ export class OlmDevice { /** * Get the right olm session id for encrypting messages to the given identity key * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {boolean} nowait Don't wait for an in-progress session to complete. + * @param nowait - Don't wait for an in-progress session to complete. * This should only be set to true of the calling function is the function * that marked the session as being in-progress. - * @param {PrefixedLogger} [log] A possibly customised log - * @return {Promise} session id, or null if no established session + * @param log - A possibly customised log + * @returns session id, or null if no established session */ public async getSessionIdForDevice( theirDeviceIdentityKey: string, @@ -756,12 +721,11 @@ export class OlmDevice { * the keys 'hasReceivedMessage' (true if the session has received an incoming * message and is therefore past the pre-key stage), and 'sessionId'. * - * @param {string} deviceIdentityKey Curve25519 identity key for the device - * @param {boolean} nowait Don't wait for an in-progress session to complete. + * @param deviceIdentityKey - Curve25519 identity key for the device + * @param nowait - Don't wait for an in-progress session to complete. * This should only be set to true of the calling function is the function * that marked the session as being in-progress. - * @param {Logger} [log] A possibly customised log - * @return {Array.<{sessionId: string, hasReceivedMessage: boolean}>} + * @param log - A possibly customised log */ public async getSessionInfoForDevice( deviceIdentityKey: string, @@ -810,21 +774,21 @@ export class OlmDevice { /** * Encrypt an outgoing message using an existing session * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {string} sessionId the id of the active session - * @param {string} payloadString payload to be encrypted and sent + * @param sessionId - the id of the active session + * @param payloadString - payload to be encrypted and sent * - * @return {Promise} ciphertext + * @returns ciphertext */ public async encryptMessage( 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,19 +804,19 @@ export class OlmDevice { }, logger.withPrefix("[encryptMessage]"), ); - return res; + return res!; } /** * Decrypt an incoming message using an existing session * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {string} sessionId the id of the active session - * @param {number} messageType messageType field from the received message - * @param {string} ciphertext base64-encoded body from the received message + * @param sessionId - the id of the active session + * @param messageType - messageType field from the received message + * @param ciphertext - base64-encoded body from the received message * - * @return {Promise} decrypted payload. + * @returns decrypted payload. */ public async decryptMessage( theirDeviceIdentityKey: string, @@ -860,7 +824,7 @@ export class OlmDevice { messageType: number, ciphertext: string, ): Promise { - let payloadString; + let payloadString: string; await this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { @@ -877,19 +841,19 @@ export class OlmDevice { }, logger.withPrefix("[decryptMessage]"), ); - return payloadString; + return payloadString!; } /** * Determine if an incoming messages is a prekey message matching an existing session * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {string} sessionId the id of the active session - * @param {number} messageType messageType field from the received message - * @param {string} ciphertext base64-encoded body from the received message + * @param sessionId - the id of the active session + * @param messageType - messageType field from the received message + * @param ciphertext - base64-encoded body from the received message * - * @return {Promise} true if the received message is a prekey message which matches + * @returns true if the received message is a prekey message which matches * the given session. */ public async matchesSession( @@ -902,7 +866,7 @@ export class OlmDevice { return false; } - let matches; + let matches: boolean; await this.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { @@ -912,7 +876,7 @@ export class OlmDevice { }, logger.withPrefix("[matchesSession]"), ); - return matches; + return matches!; } public async recordSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise { @@ -934,8 +898,7 @@ export class OlmDevice { /** * store an OutboundGroupSession in outboundGroupSessionStore * - * @param {Olm.OutboundGroupSession} session - * @private + * @internal */ private saveOutboundGroupSession(session: OutboundGroupSession): void { this.outboundGroupSessionStore[session.session_id()] = session.pickle(this.pickleKey); @@ -945,10 +908,8 @@ export class OlmDevice { * extract an OutboundGroupSession from outboundGroupSessionStore and call the * given function * - * @param {string} sessionId - * @param {function} func - * @return {object} result of func - * @private + * @returns result of func + * @internal */ private getOutboundGroupSession(sessionId: string, func: (session: OutboundGroupSession) => T): T { const pickled = this.outboundGroupSessionStore[sessionId]; @@ -968,7 +929,7 @@ export class OlmDevice { /** * Generate a new outbound group session * - * @return {string} sessionId for the outbound session. + * @returns sessionId for the outbound session. */ public createOutboundGroupSession(): string { const session = new global.Olm.OutboundGroupSession(); @@ -984,10 +945,10 @@ export class OlmDevice { /** * Encrypt an outgoing message with an outbound group session * - * @param {string} sessionId the id of the outboundgroupsession - * @param {string} payloadString payload to be encrypted and sent + * @param sessionId - the id of the outboundgroupsession + * @param payloadString - payload to be encrypted and sent * - * @return {string} ciphertext + * @returns ciphertext */ public encryptGroupMessage(sessionId: string, payloadString: string): string { logger.log(`encrypting msg with megolm session ${sessionId}`); @@ -1004,9 +965,9 @@ export class OlmDevice { /** * Get the session keys for an outbound group session * - * @param {string} sessionId the id of the outbound group session + * @param sessionId - the id of the outbound group session * - * @return {{chain_index: number, key: string}} current chain index, and + * @returns current chain index, and * base64-encoded secret key. */ public getOutboundGroupSessionKey(sessionId: string): IOutboundGroupSessionKey { @@ -1025,9 +986,9 @@ export class OlmDevice { * Unpickle a session from a sessionData object and invoke the given function. * The session is valid only until func returns. * - * @param {Object} sessionData Object describing the session. - * @param {function(Olm.InboundGroupSession)} func Invoked with the unpickled session - * @return {*} result of func + * @param sessionData - Object describing the session. + * @param func - Invoked with the unpickled session + * @returns result of func */ private unpickleInboundGroupSession( sessionData: InboundGroupSessionData, @@ -1045,15 +1006,12 @@ export class OlmDevice { /** * extract an InboundGroupSession from the crypto store and call the given function * - * @param {string} roomId The room ID to extract the session for, or null to fetch + * @param roomId - The room ID to extract the session for, or null to fetch * sessions for any room. - * @param {string} senderKey - * @param {string} sessionId - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {function(Olm.InboundGroupSession, InboundGroupSessionData)} func - * function to call. + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @param func - function to call. * - * @private + * @internal */ private getInboundGroupSession( roomId: string, @@ -1092,16 +1050,16 @@ export class OlmDevice { /** * Add an inbound group session to the session store * - * @param {string} roomId room in which this session will be used - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {Array} forwardingCurve25519KeyChain Devices involved in forwarding + * @param roomId - room in which this session will be used + * @param senderKey - base64-encoded curve25519 key of the sender + * @param forwardingCurve25519KeyChain - Devices involved in forwarding * this session to us. - * @param {string} sessionId session identifier - * @param {string} sessionKey base64-encoded secret key - * @param {Object} keysClaimed Other keys the sender claims. - * @param {boolean} exportFormat true if the megolm keys are in export format + * @param sessionId - session identifier + * @param sessionKey - base64-encoded secret key + * @param keysClaimed - Other keys the sender claims. + * @param exportFormat - true if the megolm keys are in export format * (ie, they lack an ed25519 signature) - * @param {Object} [extraSessionData={}] any other data to be include with the session + * @param extraSessionData - any other data to be include with the session */ public async addInboundGroupSession( roomId: string, @@ -1213,11 +1171,11 @@ export class OlmDevice { /** * Record in the data store why an inbound group session was withheld. * - * @param {string} roomId room that the session belongs to - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {string} code reason code - * @param {string} reason human-readable version of `code` + * @param roomId - room that the session belongs to + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param code - reason code + * @param reason - human-readable version of `code` */ public async addInboundGroupSessionWithheld( roomId: string, @@ -1245,18 +1203,14 @@ export class OlmDevice { /** * Decrypt a received message with an inbound group session * - * @param {string} roomId room in which the message was received - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {string} body base64-encoded body of the encrypted message - * @param {string} eventId ID of the event being decrypted - * @param {Number} timestamp timestamp of the event being decrypted + * @param roomId - room in which the message was received + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param body - base64-encoded body of the encrypted message + * @param eventId - ID of the event being decrypted + * @param timestamp - timestamp of the event being decrypted * - * @return {null} the sessionId is unknown - * - * @return {Promise<{result: string, senderKey: string, - * forwardingCurve25519KeyChain: Array, - * keysClaimed: Object}>} + * @returns null if the sessionId is unknown */ public async decryptGroupMessage( roomId: string, @@ -1293,7 +1247,7 @@ export class OlmDevice { result = null; return; } - let res; + let res: ReturnType; try { res = session.decrypt(body); } catch (e) { @@ -1313,8 +1267,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 @@ -1372,11 +1326,11 @@ export class OlmDevice { /** * Determine if we have the keys for a given megolm session * - * @param {string} roomId room in which the message was received - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier + * @param roomId - room in which the message was received + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier * - * @returns {Promise} true if we have the keys to this session + * @returns true if we have the keys to this session */ public async hasInboundSessionKeys(roomId: string, senderKey: string, sessionId: string): Promise { let result: boolean; @@ -1415,16 +1369,13 @@ export class OlmDevice { /** * Extract the keys to a given megolm session, for sharing * - * @param {string} roomId room in which the message was received - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {number} chainIndex The chain index at which to export the session. + * @param roomId - room in which the message was received + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param chainIndex - The chain index at which to export the session. * If omitted, export at the first index we know about. * - * @returns {Promise<{chain_index: number, key: string, - * forwarding_curve25519_key_chain: Array, - * sender_claimed_ed25519_key: string - * }>} + * @returns * details of the session key. The key is a base64-encoded megolm key in * export format. * @@ -1489,10 +1440,10 @@ export class OlmDevice { /** * Export an inbound group session * - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {ISessionInfo} sessionData The session object from the store - * @return {module:crypto/OlmDevice.MegolmSessionData} exported session data + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param sessionData - The session object from the store + * @returns exported session data */ public exportInboundGroupSession( senderKey: string, @@ -1536,11 +1487,11 @@ export class OlmDevice { /** * Verify an ed25519 signature. * - * @param {string} key ed25519 key - * @param {string} message message which was signed - * @param {string} signature base64-encoded signature to be checked + * @param key - ed25519 key + * @param message - message which was signed + * @param signature - base64-encoded signature to be checked * - * @raises {Error} if there is a problem with the verification. If the key was + * @throws Error if there is a problem with the verification. If the key was * too small then the message will be "OLM.INVALID_BASE64". If the signature * was invalid then the message will be "OLM.BAD_MESSAGE_MAC". */ @@ -1555,7 +1506,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.", @@ -1565,11 +1516,11 @@ export const WITHHELD_MESSAGES = { /** * Calculate the message to use for the exception when a session key is withheld. * - * @param {object} withheld An object that describes why the key was withheld. + * @param withheld - An object that describes why the key was withheld. * - * @return {string} the message + * @returns the message * - * @private + * @internal */ function calculateWithheldMessage(withheld: IWithheld): string { if (withheld.code && withheld.code in WITHHELD_MESSAGES) { diff --git a/src/crypto/OutgoingRoomKeyRequestManager.ts b/src/crypto/OutgoingRoomKeyRequestManager.ts index 427234347..833592a4c 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.ts +++ b/src/crypto/OutgoingRoomKeyRequestManager.ts @@ -14,28 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { v4 as uuidv4 } from "uuid"; + import { logger } from '../logger'; import { MatrixClient } from "../client"; import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index"; import { CryptoStore, OutgoingRoomKeyRequest } from './store/base'; -import { EventType } from "../@types/event"; +import { EventType, ToDeviceMessageId } from "../@types/event"; /** * Internal module. Management of outgoing room key requests. * * See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ * for draft documentation on what we're supposed to be implementing here. - * - * @module */ // delay between deciding we want some keys, and sending out the request, to // allow for (a) it turning up anyway, (b) grouping requests together const SEND_KEY_REQUESTS_DELAY_MS = 500; -/** possible states for a room key request +/** + * possible states for a room key request * * The state machine looks like: + * ``` * * | (cancellation sent) * | .-------------------------------------------------. @@ -58,8 +60,7 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500; * | (cancellation sent) | * V | * (deleted) <---------------------------+ - * - * @enum {number} + * ``` */ export enum RoomKeyRequestState { /** request not yet sent */ @@ -132,12 +133,10 @@ export class OutgoingRoomKeyRequestManager { * Otherwise, a request is added to the pending list, and a job is started * in the background to send it. * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * @param {Array<{userId: string, deviceId: string}>} recipients - * @param {boolean} resend whether to resend the key request if there is + * @param resend - whether to resend the key request if there is * already one * - * @returns {Promise} resolves when the request has been added to the + * @returns resolves when the request has been added to the * pending list (or we have established that a similar request already * exists) */ @@ -237,9 +236,8 @@ export class OutgoingRoomKeyRequestManager { /** * Cancel room key requests, if any match the given requestBody * - * @param {module:crypto~RoomKeyRequestBody} requestBody * - * @returns {Promise} resolves when the request has been updated in our + * @returns resolves when the request has been updated in our * pending list. */ public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { @@ -322,11 +320,10 @@ export class OutgoingRoomKeyRequestManager { /** * Look for room key requests by target device and state * - * @param {string} userId Target user ID - * @param {string} deviceId Target device ID + * @param userId - Target user ID + * @param deviceId - Target device ID * - * @return {Promise} resolves to a list of all the - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to a list of all the {@link OutgoingRoomKeyRequest} */ public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): Promise { return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]); @@ -337,7 +334,7 @@ export class OutgoingRoomKeyRequestManager { * This is intended for situations where something substantial has changed, and we * don't really expect the other end to even care about the cancellation. * For example, after initialization or self-verification. - * @return {Promise} An array of `queueRoomKeyRequest` outputs. + * @returns An array of `queueRoomKeyRequest` outputs. */ public async cancelAndResendAllOutgoingRequests(): Promise { const outgoings = await this.cryptoStore.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent); @@ -483,7 +480,10 @@ export class OutgoingRoomKeyRequestManager { if (!contentMap[recip.userId]) { contentMap[recip.userId] = {}; } - contentMap[recip.userId][recip.deviceId] = message; + contentMap[recip.userId][recip.deviceId] = { + ...message, + [ToDeviceMessageId]: uuidv4(), + }; } return this.baseApis.sendToDevice(EventType.RoomKeyRequest, contentMap, txnId); diff --git a/src/crypto/RoomList.ts b/src/crypto/RoomList.ts index 672ef473c..2f5233bd2 100644 --- a/src/crypto/RoomList.ts +++ b/src/crypto/RoomList.ts @@ -15,8 +15,6 @@ limitations under the License. */ /** - * @module crypto/RoomList - * * Manages the list of encrypted rooms */ @@ -31,9 +29,6 @@ export interface IRoomEncryption { } /* eslint-enable camelcase */ -/** - * @alias module:crypto/RoomList - */ export class RoomList { // Object of roomId -> room e2e info object (body of the m.room.encryption event) private roomEncryption: Record = {}; diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index 5c13ba4b0..921207c4a 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -14,17 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ +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'; import { TypedEventEmitter } from '../models/typed-event-emitter'; import { defer, IDeferred } from "../utils"; +import { ToDeviceMessageId } from "../@types/event"; export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; @@ -58,14 +60,12 @@ interface IDecryptors { interface ISecretInfo { encrypted: { - // eslint-disable-next-line camelcase - key_id: IEncryptedPayload; + [keyId: string]: IEncryptedPayload; }; } /** * Implements Secure Secret Storage and Sharing (MSC1946) - * @module crypto/SecretStorage */ export class SecretStorage { private requests = new Map(); @@ -118,15 +118,15 @@ export class SecretStorage { /** * Add a key for encrypting secrets. * - * @param {string} algorithm the algorithm used by the key. - * @param {object} opts the options for the algorithm. The properties used + * @param algorithm - the algorithm used by the key. + * @param opts - the options for the algorithm. The properties used * depend on the algorithm given. - * @param {string} [keyId] the ID of the key. If not given, a random + * @param keyId - the ID of the key. If not given, a random * ID will be generated. * - * @return {object} An object with: - * keyId: {string} the ID of the key - * keyInfo: {object} details about the key (iv, mac, passphrase) + * @returns An object with: + * keyId: the ID of the key + * keyInfo: details about the key (iv, mac, passphrase) */ public async addKey( algorithm: string, @@ -175,9 +175,9 @@ export class SecretStorage { /** * Get the key information for a given ID. * - * @param {string} [keyId = default key's ID] The ID of the key to check + * @param keyId - The ID of the key to check * for. Defaults to the default key ID if not provided. - * @returns {Array?} If the key was found, the return value is an array of + * @returns If the key was found, the return value is an array of * the form [keyId, keyInfo]. Otherwise, null is returned. * XXX: why is this an array when addKey returns an object? */ @@ -198,9 +198,9 @@ export class SecretStorage { /** * Check whether we have a key with a given ID. * - * @param {string} [keyId = default key's ID] The ID of the key to check + * @param keyId - The ID of the key to check * for. Defaults to the default key ID if not provided. - * @return {boolean} Whether we have the key. + * @returns Whether we have the key. */ public async hasKey(keyId?: string): Promise { return Boolean(await this.getKey(keyId)); @@ -209,10 +209,10 @@ export class SecretStorage { /** * Check whether a key matches what we expect based on the key info * - * @param {Uint8Array} key the key to check - * @param {object} info the key info + * @param key - the key to check + * @param info - the key info * - * @return {boolean} whether or not the key matches + * @returns whether or not the key matches */ public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise { if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { @@ -231,9 +231,9 @@ export class SecretStorage { /** * Store an encrypted secret on the server * - * @param {string} name The name of the secret - * @param {string} secret The secret contents. - * @param {Array} keys The IDs of the keys to use to encrypt the secret + * @param name - The name of the secret + * @param secret - The secret contents. + * @param keys - The IDs of the keys to use to encrypt the secret * or null/undefined to use the default key. */ public async store(name: string, secret: string, keys?: string[] | null): Promise { @@ -279,9 +279,9 @@ export class SecretStorage { /** * Get a secret from storage. * - * @param {string} name the name of the secret + * @param name - the name of the secret * - * @return {string} the contents of the secret + * @returns the contents of the secret */ public async get(name: string): Promise { const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); @@ -315,31 +315,19 @@ 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); } /** * Check if a secret is stored on the server. * - * @param {string} name the name of the secret + * @param name - the name of the secret * - * @return {object?} map of key name to key info the secret is encrypted + * @returns map of key name to key info the secret is encrypted * with, or null if it is not present or not encrypted with a trusted * key */ @@ -348,7 +336,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)) { @@ -372,8 +360,8 @@ export class SecretStorage { /** * Request a secret from another device * - * @param {string} name the name of the secret to request - * @param {string[]} devices the devices to request the secret from + * @param name - the name of the secret to request + * @param devices - the devices to request the secret from */ public request(this: SecretStorage, name: string, devices: string[]): ISecretRequest { const requestId = this.baseApis.makeTxnId(); @@ -388,7 +376,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; } @@ -407,8 +395,9 @@ export class SecretStorage { action: "request", requesting_device_id: this.baseApis.deviceId, request_id: requestId, + [ToDeviceMessageId]: uuidv4(), }; - const toDevice = {}; + const toDevice: Record = {}; for (const device of devices) { toDevice[device] = requestData; } @@ -486,10 +475,11 @@ 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(), }; await olmlib.ensureOlmSessionsForDevices( this.baseApis.crypto!.olmDevice, diff --git a/src/crypto/aes.ts b/src/crypto/aes.ts index 33971e634..da5464834 100644 --- a/src/crypto/aes.ts +++ b/src/crypto/aes.ts @@ -22,18 +22,21 @@ const zeroSalt = new Uint8Array(8); export interface IEncryptedPayload { [key: string]: any; // extensible + /** the initialization vector in base64 */ iv: string; + /** the ciphertext in base64 */ ciphertext: string; + /** the HMAC in base64 */ mac: string; } /** * encrypt a string * - * @param {string} data the plaintext to encrypt - * @param {Uint8Array} key the encryption key to use - * @param {string} name the name of the secret - * @param {string} ivStr the initialization vector to use + * @param data - the plaintext to encrypt + * @param key - the encryption key to use + * @param name - the name of the secret + * @param ivStr - the initialization vector to use */ export async function encryptAES( data: string, @@ -83,12 +86,9 @@ export async function encryptAES( /** * decrypt a string * - * @param {object} data the encrypted data - * @param {string} data.ciphertext the ciphertext in base64 - * @param {string} data.iv the initialization vector in base64 - * @param {string} data.mac the HMAC in base64 - * @param {Uint8Array} key the encryption key to use - * @param {string} name the name of the secret + * @param data - the encrypted data + * @param key - the encryption key to use + * @param name - the name of the secret */ export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise { const [aesKey, hmacKey] = await deriveKeys(key, name); @@ -168,10 +168,10 @@ const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0 /** Calculate the MAC for checking the key. * - * @param {Uint8Array} key the key to use - * @param {string} [iv] The initialization vector as a base64-encoded string. + * @param key - the key to use + * @param iv - The initialization vector as a base64-encoded string. * If omitted, a random initialization vector will be created. - * @return {Promise} An object that contains, `mac` and `iv` properties. + * @returns An object that contains, `mac` and `iv` properties. */ export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise { return encryptAES(ZERO_STR, key, "", iv); diff --git a/src/crypto/algorithms/base.ts b/src/crypto/algorithms/base.ts index d6c70bc10..3229ad39b 100644 --- a/src/crypto/algorithms/base.ts +++ b/src/crypto/algorithms/base.ts @@ -16,59 +16,53 @@ limitations under the License. /** * Internal module. Defines the base classes of the encryption implementations - * - * @module */ 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"; /** - * map of registered encryption algorithm classes. A map from string to {@link - * module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class - * - * @type {Object.} + * Map of registered encryption algorithm classes. A map from string to {@link EncryptionAlgorithm} class */ export const ENCRYPTION_CLASSES = new Map EncryptionAlgorithm>(); export type DecryptionClassParams

= Omit; /** - * map of registered encryption algorithm classes. Map from string to {@link - * module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class - * - * @type {Object.} + * map of registered encryption algorithm classes. Map from string to {@link DecryptionAlgorithm} class */ export const DECRYPTION_CLASSES = new Map DecryptionAlgorithm>(); export interface IParams { + /** The UserID for the local user */ userId: string; + /** The identifier for this device. */ deviceId: string; + /** crypto core */ crypto: Crypto; + /** olm.js wrapper */ olmDevice: OlmDevice; + /** base matrix api interface */ baseApis: MatrixClient; + /** The ID of the room we will be sending to */ roomId?: string; + /** The body of the m.room.encryption event */ config: IRoomEncryption & object; } /** * base type for encryption implementations - * - * @alias module:crypto/algorithms/base.EncryptionAlgorithm - * - * @param {object} params parameters - * @param {string} params.userId The UserID for the local user - * @param {string} params.deviceId The identifier for this device. - * @param {module:crypto} params.crypto crypto core - * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper - * @param {MatrixClient} baseApis base matrix api interface - * @param {string} params.roomId The ID of the room we will be sending to - * @param {object} params.config The body of the m.room.encryption event */ export abstract class EncryptionAlgorithm { protected readonly userId: string; @@ -78,6 +72,9 @@ export abstract class EncryptionAlgorithm { protected readonly baseApis: MatrixClient; protected readonly roomId?: string; + /** + * @param params - parameters + */ public constructor(params: IParams) { this.userId = params.userId; this.deviceId = params.deviceId; @@ -91,33 +88,28 @@ export abstract class EncryptionAlgorithm { * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. * - * @param {module:models/room} room the room the event is in + * @param room - the room the event is in */ public prepareToEncrypt(room: Room): void {} /** * Encrypt a message event * - * @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage * @public - * @abstract * - * @param {module:models/room} room - * @param {string} eventType - * @param {object} content event content + * @param content - event content * - * @return {Promise} Promise which resolves to the new event body + * @returns 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. * - * @param {module:models/event.MatrixEvent} event event causing the change - * @param {module:models/room-member} member user whose membership changed - * @param {string=} oldMembership previous membership + * @param event - event causing the change + * @param member - user whose membership changed + * @param oldMembership - previous membership * @public - * @abstract */ public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {} @@ -133,15 +125,6 @@ export abstract class EncryptionAlgorithm { /** * base type for decryption implementations - * - * @alias module:crypto/algorithms/base.DecryptionAlgorithm - * @param {object} params parameters - * @param {string} params.userId The UserID for the local user - * @param {module:crypto} params.crypto crypto core - * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper - * @param {MatrixClient} baseApis base matrix api interface - * @param {string=} params.roomId The ID of the room we will be receiving - * from. Null for to-device events. */ export abstract class DecryptionAlgorithm { protected readonly userId: string; @@ -161,12 +144,9 @@ export abstract class DecryptionAlgorithm { /** * Decrypt an event * - * @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent - * @abstract + * @param event - undecrypted event * - * @param {MatrixEvent} event undecrypted event - * - * @return {Promise} promise which + * @returns promise which * resolves once we have finished decrypting. Rejects with an * `algorithms.DecryptionError` if there is a problem decrypting the event. */ @@ -175,9 +155,7 @@ export abstract class DecryptionAlgorithm { /** * Handle a key event * - * @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent - * - * @param {module:models/event.MatrixEvent} params event key event + * @param params - event key event */ public async onRoomKeyEvent(params: MatrixEvent): Promise { // ignore by default @@ -186,8 +164,7 @@ export abstract class DecryptionAlgorithm { /** * Import a room key * - * @param {module:crypto/OlmDevice.MegolmSessionData} session - * @param {object} opts object + * @param opts - object */ public async importRoomKey(session: IMegolmSessionData, opts: object): Promise { // ignore by default @@ -196,8 +173,7 @@ export abstract class DecryptionAlgorithm { /** * Determine if we have the keys necessary to respond to a room key request * - * @param {module:crypto~IncomingRoomKeyRequest} keyRequest - * @return {Promise} true if we have the keys and could (theoretically) share + * @returns true if we have the keys and could (theoretically) share * them; else false. */ public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise { @@ -207,7 +183,6 @@ export abstract class DecryptionAlgorithm { /** * Send the response to a room key request * - * @param {module:crypto~IncomingRoomKeyRequest} keyRequest */ public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void { throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm"); @@ -217,7 +192,7 @@ export abstract class DecryptionAlgorithm { * Retry decrypting all the events from a sender that haven't been * decrypted yet. * - * @param {string} senderKey the sender's key + * @param senderKey - the sender's key */ public async retryDecryptionFromSender(senderKey: string): Promise { // ignore by default @@ -231,13 +206,10 @@ export abstract class DecryptionAlgorithm { /** * Exception thrown when decryption fails * - * @alias module:crypto/algorithms/base.DecryptionError - * @param {string} msg user-visible message describing the problem + * @param msg - user-visible message describing the problem * - * @param {Object=} details key/value pairs reported in the logs but not shown + * @param details - key/value pairs reported in the logs but not shown * to the user. - * - * @extends Error */ export class DecryptionError extends Error { public readonly detailedString: string; @@ -262,16 +234,14 @@ function detailedStringForDecryptionError(err: DecryptionError, details?: Record return result; } -/** - * Exception thrown specifically when we want to warn the user to consider - * the security of their conversation before continuing - * - * @param {string} msg message describing the problem - * @param {Object} devices userId -> {deviceId -> object} - * set of unknown devices per user we're warning about - * @extends Error - */ export class UnknownDeviceError extends Error { + /** + * Exception thrown specifically when we want to warn the user to consider + * the security of their conversation before continuing + * + * @param msg - message describing the problem + * @param devices - set of unknown devices per user we're warning about + */ public constructor( msg: string, public readonly devices: Record>, @@ -286,15 +256,11 @@ export class UnknownDeviceError extends Error { /** * Registers an encryption/decryption class for a particular algorithm * - * @param {string} algorithm algorithm tag to register for + * @param algorithm - algorithm tag to register for * - * @param {class} encryptor {@link - * module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} - * implementation + * @param encryptor - {@link EncryptionAlgorithm} implementation * - * @param {class} decryptor {@link - * module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} - * implementation + * @param decryptor - {@link DecryptionAlgorithm} implementation */ export function registerAlgorithm

( algorithm: string, diff --git a/src/crypto/algorithms/index.ts b/src/crypto/algorithms/index.ts index 3dd1158a0..b3c5b0ede 100644 --- a/src/crypto/algorithms/index.ts +++ b/src/crypto/algorithms/index.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module crypto/algorithms - */ - import "./olm"; import "./megolm"; diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index cbad327a6..d995623d4 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -16,10 +16,10 @@ limitations under the License. /** * Defines m.olm encryption/decryption - * - * @module crypto/algorithms/megolm */ +import { v4 as uuidv4 } from "uuid"; + import { logger } from '../../logger'; import * as olmlib from "../olmlib"; import { @@ -36,9 +36,15 @@ import { Room } from '../../models/room'; import { DeviceInfo } from "../deviceinfo"; import { IOlmSessionResult } from "../olmlib"; import { DeviceInfoMap } from "../DeviceList"; -import { MatrixEvent } from "../../models/event"; -import { EventType, MsgType } from '../../@types/event'; -import { IEncryptedContent, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index"; +import { IContent, MatrixEvent } from "../../models/event"; +import { EventType, MsgType, ToDeviceMessageId } from '../../@types/event'; +import { + IMegolmEncryptedContent, + IEventDecryptionResult, + IMegolmSessionData, + IncomingRoomKeyRequest, + IEncryptedContent, +} from "../index"; import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager'; import { OlmGroupSessionExtraData } from "../../@types/crypto"; import { MatrixError } from "../../http-api"; @@ -115,37 +121,27 @@ interface SharedWithData { } /** - * @private - * @constructor - * - * @param {string} sessionId - * @param {boolean} sharedHistory whether the session can be freely shared with - * other group members, according to the room history visibility settings - * - * @property {string} sessionId - * @property {Number} useCount number of times this session has been used - * @property {Number} creationTime when the session was created (ms since the epoch) - * - * @property {object} sharedWithDevices - * devices with which we have shared the session key - * userId -> {deviceId -> SharedWithData} + * @internal */ class OutboundSessionInfo { + /** number of times this session has been used */ public useCount = 0; + /** when the session was created (ms since the epoch) */ public creationTime: number; + /** devices with which we have shared the session key `userId -> {deviceId -> SharedWithData}` */ public sharedWithDevices: Record> = {}; public blockedDevicesNotified: Record> = {}; + /** + * @param sharedHistory - whether the session can be freely shared with + * other group members, according to the room history visibility settings + */ public constructor(public readonly sessionId: string, public readonly sharedHistory = false) { this.creationTime = new Date().getTime(); } /** * Check if it's time to rotate the session - * - * @param {Number} rotationPeriodMsgs - * @param {Number} rotationPeriodMs - * @return {Boolean} */ public needsRotation(rotationPeriodMsgs: number, rotationPeriodMs: number): boolean { const sessionLifetime = new Date().getTime() - this.creationTime; @@ -181,10 +177,10 @@ class OutboundSessionInfo { * Determine if this session has been shared with devices which it shouldn't * have been. * - * @param {Object} devicesInRoom userId -> {deviceId -> object} + * @param devicesInRoom - `userId -> {deviceId -> object}` * devices we should shared the session with. * - * @return {Boolean} true if we have shared the session with devices which aren't + * @returns true if we have shared the session with devices which aren't * in devicesInRoom. */ public sharedWithTooManyDevices(devicesInRoom: Record>): boolean { @@ -220,13 +216,9 @@ class OutboundSessionInfo { /** * Megolm encryption implementation * - * @constructor - * @extends {module:crypto/algorithms/EncryptionAlgorithm} - * - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/EncryptionAlgorithm} + * @param params - parameters, as per {@link 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 @@ -257,15 +249,14 @@ class MegolmEncryption extends EncryptionAlgorithm { } /** - * @private + * @internal * - * @param {module:models/room} room - * @param {Object} devicesInRoom The devices in this room, indexed by user ID - * @param {Object} blocked The devices that are blocked, indexed by user ID - * @param {boolean} [singleOlmCreationPhase] Only perform one round of olm + * @param devicesInRoom - The devices in this room, indexed by user ID + * @param blocked - The devices that are blocked, indexed by user ID + * @param singleOlmCreationPhase - Only perform one round of olm * session creation * - * @return {Promise} Promise which resolves to the + * @returns Promise which resolves to the * OutboundSessionInfo when setup is complete. */ private async ensureOutboundSession( @@ -480,11 +471,10 @@ class MegolmEncryption extends EncryptionAlgorithm { } /** - * @private + * @internal * - * @param {boolean} sharedHistory * - * @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * @returns session */ private async prepareNewSession(sharedHistory: boolean): Promise { const sessionId = this.olmDevice.createOutboundGroupSession(); @@ -506,15 +496,15 @@ class MegolmEncryption extends EncryptionAlgorithm { * Determines what devices in devicesByUser don't have an olm session as given * in devicemap. * - * @private + * @internal * - * @param {object} devicemap the devices that have olm sessions, as returned by + * @param devicemap - the devices that have olm sessions, as returned by * olmlib.ensureOlmSessionsForDevices. - * @param {object} devicesByUser a map of user IDs to array of deviceInfo - * @param {array} [noOlmDevices] an array to fill with devices that don't have + * @param devicesByUser - a map of user IDs to array of deviceInfo + * @param noOlmDevices - an array to fill with devices that don't have * olm sessions * - * @return {array} an array of devices that don't have olm sessions. If + * @returns an array of devices that don't have olm sessions. If * noOlmDevices is specified, then noOlmDevices will be returned. */ private getDevicesWithoutSessions( @@ -550,11 +540,11 @@ class MegolmEncryption extends EncryptionAlgorithm { * Splits the user device map into multiple chunks to reduce the number of * devices we encrypt to per API call. * - * @private + * @internal * - * @param {object} devicesByUser map from userid to list of devices + * @param devicesByUser - map from userid to list of devices * - * @return {array>} the blocked devices, split into chunks + * @returns the blocked devices, split into chunks */ private splitDevices( devicesByUser: Record>, @@ -591,18 +581,16 @@ class MegolmEncryption extends EncryptionAlgorithm { } /** - * @private + * @internal * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {number} chainIndex current chain index + * @param chainIndex - current chain index * - * @param {object} userDeviceMap - * mapping from userId to deviceInfo + * @param userDeviceMap - mapping from userId to deviceInfo * - * @param {object} payload fields to include in the encrypted payload + * @param payload - fields to include in the encrypted payload * - * @return {Promise} Promise which resolves once the key sharing + * @returns Promise which resolves once the key sharing * for the given userDeviceMap is generated and has been sent. */ private encryptAndSendKeysToDevices( @@ -631,15 +619,14 @@ class MegolmEncryption extends EncryptionAlgorithm { } /** - * @private + * @internal * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {array} userDeviceMap list of blocked devices to notify + * @param userDeviceMap - list of blocked devices to notify * - * @param {object} payload fields to include in the notification payload + * @param payload - fields to include in the notification payload * - * @return {Promise} Promise which resolves once the notifications + * @returns Promise which resolves once the notifications * for the given userDeviceMap is generated and has been sent. */ private async sendBlockedNotificationsToDevices( @@ -655,9 +642,13 @@ class MegolmEncryption extends EncryptionAlgorithm { const deviceInfo = blockedInfo.deviceInfo; const deviceId = deviceInfo.deviceId; - const message = Object.assign({}, payload); - message.code = blockedInfo.code; - message.reason = blockedInfo.reason; + const message = { + ...payload, + code: blockedInfo.code, + reason: blockedInfo.reason, + [ToDeviceMessageId]: uuidv4(), + }; + if (message.code === "m.no_olm") { delete message.room_id; delete message.session_id; @@ -683,10 +674,10 @@ class MegolmEncryption extends EncryptionAlgorithm { * Re-shares a megolm session key with devices if the key has already been * sent to them. * - * @param {string} senderKey The key of the originating device for the session - * @param {string} sessionId ID of the outbound session to share - * @param {string} userId ID of the user who owns the target device - * @param {module:crypto/deviceinfo} device The target device + * @param senderKey - The key of the originating device for the session + * @param sessionId - ID of the outbound session to share + * @param userId - ID of the user who owns the target device + * @param device - The target device */ public async reshareKeyWithDevice( senderKey: string, @@ -755,10 +746,11 @@ 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(), }; await olmlib.encryptMessageForDevice( encryptedContent.ciphertext, @@ -779,26 +771,23 @@ class MegolmEncryption extends EncryptionAlgorithm { } /** - * @private + * @internal * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {object} key the session key as returned by + * @param key - the session key as returned by * OlmDevice.getOutboundGroupSessionKey * - * @param {object} payload the base to-device message payload for sharing keys + * @param payload - the base to-device message payload for sharing keys * - * @param {object} devicesByUser - * map from userid to list of devices + * @param devicesByUser - map from userid to list of devices * - * @param {array} errorDevices - * array that will be populated with the devices that we can't get an + * @param errorDevices - array that will be populated with the devices that we can't get an * olm session for * - * @param {Number} [otkTimeout] The timeout in milliseconds when requesting + * @param otkTimeout - The timeout in milliseconds when requesting * one-time keys for establishing new olm sessions. * - * @param {Array} [failedServers] An array to fill with remote servers that + * @param failedServers - An array to fill with remote servers that * failed to respond to one-time-key requests. */ private async shareKeyWithDevices( @@ -853,11 +842,9 @@ class MegolmEncryption extends EncryptionAlgorithm { /** * Notify devices that we weren't able to create olm sessions. * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {object} key * - * @param {Array} failedDevices the devices that we were unable to + * @param failedDevices - the devices that we were unable to * create olm sessions for, as returned by shareKeyWithDevices */ private async notifyFailedOlmDevices( @@ -914,10 +901,8 @@ class MegolmEncryption extends EncryptionAlgorithm { /** * Notify blocked devices that they have been blocked. * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {object} devicesByUser - * map from userid to device ID to blocked data + * @param devicesByUser - map from userid to device ID to blocked data */ private async notifyBlockedDevices( session: OutboundSessionInfo, @@ -950,7 +935,7 @@ class MegolmEncryption extends EncryptionAlgorithm { * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. * - * @param {module:models/room} room the room the event is in + * @param room - the room the event is in */ public prepareToEncrypt(room: Room): void { if (this.encryptionPreparation != null) { @@ -995,15 +980,11 @@ class MegolmEncryption extends EncryptionAlgorithm { } /** - * @inheritdoc + * @param content - plaintext event content * - * @param {module:models/room} room - * @param {string} eventType - * @param {object} content plaintext event content - * - * @return {Promise} Promise which resolves to the new event body + * @returns 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) { @@ -1038,12 +1019,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 @@ -1057,7 +1036,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: @@ -1092,7 +1071,7 @@ class MegolmEncryption extends EncryptionAlgorithm { * unknown to the user. If so, warn the user, and mark them as known to * give the user a chance to go verify them before re-sending this message. * - * @param {Object} devicesInRoom userId -> {deviceId -> object} + * @param devicesInRoom - `userId -> {deviceId -> object}` * devices we should shared the session with. */ private checkForUnknownDevices(devicesInRoom: DeviceInfoMap): void { @@ -1122,7 +1101,7 @@ class MegolmEncryption extends EncryptionAlgorithm { * Remove unknown devices from a set of devices. The devicesInRoom parameter * will be modified. * - * @param {Object} devicesInRoom userId -> {deviceId -> object} + * @param devicesInRoom - `userId -> {deviceId -> object}` * devices we should shared the session with. */ private removeUnknownDevices(devicesInRoom: DeviceInfoMap): void { @@ -1142,11 +1121,10 @@ class MegolmEncryption extends EncryptionAlgorithm { /** * Get the list of unblocked devices for all users in the room * - * @param {module:models/room} room - * @param forceDistributeToUnverified if set to true will include the unverified devices + * @param forceDistributeToUnverified - if set to true will include the unverified devices * even if setting is set to block them (useful for verification) * - * @return {Promise} Promise which resolves to an array whose + * @returns Promise which resolves to an array whose * first element is a map from userId to deviceId to deviceInfo indicating * the devices that messages should be encrypted to, and whose second * element is a map from userId to deviceId to data indicating the devices @@ -1214,13 +1192,9 @@ class MegolmEncryption extends EncryptionAlgorithm { /** * Megolm decryption implementation * - * @constructor - * @extends {module:crypto/algorithms/DecryptionAlgorithm} - * - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/DecryptionAlgorithm} + * @param params - parameters, as per {@link 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 @@ -1237,12 +1211,8 @@ class MegolmDecryption extends DecryptionAlgorithm { } /** - * @inheritdoc - * - * @param {MatrixEvent} event - * * returns a promise which resolves to a - * {@link module:crypto~EventDecryptionResult} once we have finished + * {@link EventDecryptionResult} once we have finished * decrypting, or rejects with an `algorithms.DecryptionError` if there is a * problem decrypting the event. */ @@ -1383,9 +1353,8 @@ class MegolmDecryption extends DecryptionAlgorithm { /** * Add an event to the list of those awaiting their session keys. * - * @private + * @internal * - * @param {module:models/event.MatrixEvent} event */ private addEventToPendingList(event: MatrixEvent): void { const content = event.getWireContent(); @@ -1404,9 +1373,8 @@ class MegolmDecryption extends DecryptionAlgorithm { /** * Remove an event from the list of those awaiting their session keys. * - * @private + * @internal * - * @param {module:models/event.MatrixEvent} event */ private removeEventFromPendingList(event: MatrixEvent): void { const content = event.getWireContent(); @@ -1427,11 +1395,6 @@ class MegolmDecryption extends DecryptionAlgorithm { } } - /** - * @inheritdoc - * - * @param {module:models/event.MatrixEvent} event key event - */ public async onRoomKeyEvent(event: MatrixEvent): Promise { const content = event.getContent>(); let senderKey = event.getSenderKey()!; @@ -1608,9 +1571,7 @@ class MegolmDecryption extends DecryptionAlgorithm { } /** - * @inheritdoc - * - * @param {module:models/event.MatrixEvent} event key event + * @param event - key event */ public async onRoomKeyWithheldEvent(event: MatrixEvent): Promise { const content = event.getContent(); @@ -1663,10 +1624,11 @@ 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(), }; await olmlib.encryptMessageForDevice( encryptedContent.ciphertext, @@ -1694,9 +1656,6 @@ class MegolmDecryption extends DecryptionAlgorithm { } } - /** - * @inheritdoc - */ public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise { const body = keyRequest.requestBody; @@ -1708,9 +1667,6 @@ class MegolmDecryption extends DecryptionAlgorithm { ); } - /** - * @inheritdoc - */ public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void { const userId = keyRequest.userId; const deviceId = keyRequest.deviceId; @@ -1744,10 +1700,11 @@ 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(), }; return this.olmlib.encryptMessageForDevice( @@ -1795,19 +1752,15 @@ class MegolmDecryption extends DecryptionAlgorithm { } /** - * @inheritdoc - * - * @param {module:crypto/OlmDevice.MegolmSessionData} session - * @param {object} [opts={}] options for the import - * @param {boolean} [opts.untrusted] whether the key should be considered as untrusted - * @param {string} [opts.source] where the key came from + * @param untrusted - whether the key should be considered as untrusted + * @param source - where the key came from */ public importRoomKey( session: IMegolmSessionData, - opts: { untrusted?: boolean, source?: string } = {}, + { untrusted, source }: { untrusted?: boolean, source?: string } = {}, ): Promise { const extraSessionData: OlmGroupSessionExtraData = {}; - if (opts.untrusted || session.untrusted) { + if (untrusted || session.untrusted) { extraSessionData.untrusted = true; } if (session["org.matrix.msc3061.shared_history"]) { @@ -1823,7 +1776,7 @@ class MegolmDecryption extends DecryptionAlgorithm { true, extraSessionData, ).then(() => { - if (opts.source !== "backup") { + if (source !== "backup") { // don't wait for it to complete this.crypto.backupManager.backupGroupSession( session.sender_key, session.session_id, @@ -1842,13 +1795,11 @@ class MegolmDecryption extends DecryptionAlgorithm { * Have another go at decrypting events after we receive a key. Resolves once * decryption has been re-attempted on all events. * - * @private - * @param {String} senderKey - * @param {String} sessionId - * @param {Boolean} forceRedecryptIfUntrusted whether messages that were already + * @internal + * @param forceRedecryptIfUntrusted - whether messages that were already * successfully decrypted using untrusted keys should be re-decrypted * - * @return {Boolean} whether all messages were successfully + * @returns whether all messages were successfully * decrypted with trusted keys */ private async retryDecryption( @@ -1923,6 +1874,7 @@ class MegolmDecryption extends DecryptionAlgorithm { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: {}, + [ToDeviceMessageId]: uuidv4(), }; contentMap[userId][deviceInfo.deviceId] = encryptedContent; promises.push( diff --git a/src/crypto/algorithms/olm.ts b/src/crypto/algorithms/olm.ts index 682aa4c7c..1feee6ba6 100644 --- a/src/crypto/algorithms/olm.ts +++ b/src/crypto/algorithms/olm.ts @@ -16,8 +16,6 @@ limitations under the License. /** * Defines m.olm encryption/decryption - * - * @module crypto/algorithms/olm */ import { logger } from '../../logger'; @@ -30,13 +28,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; } @@ -44,21 +42,17 @@ interface IMessage { /** * Olm encryption implementation * - * @constructor - * @extends {module:crypto/algorithms/EncryptionAlgorithm} - * - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/EncryptionAlgorithm} + * @param params - parameters, as per {@link EncryptionAlgorithm} */ class OlmEncryption extends EncryptionAlgorithm { private sessionPrepared = false; private prepPromise: Promise | null = null; /** - * @private + * @internal - * @param {string[]} roomMembers list of currently-joined users in the room - * @return {Promise} Promise which resolves when setup is complete + * @param roomMembers - list of currently-joined users in the room + * @returns Promise which resolves when setup is complete */ private ensureSession(roomMembers: string[]): Promise { if (this.prepPromise) { @@ -83,15 +77,11 @@ class OlmEncryption extends EncryptionAlgorithm { } /** - * @inheritdoc + * @param content - plaintext event content * - * @param {module:models/room} room - * @param {string} eventType - * @param {object} content plaintext event content - * - * @return {Promise} Promise which resolves to the new event body + * @returns 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 +101,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: {}, }; @@ -150,19 +140,12 @@ class OlmEncryption extends EncryptionAlgorithm { /** * Olm decryption implementation * - * @constructor - * @extends {module:crypto/algorithms/DecryptionAlgorithm} - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/DecryptionAlgorithm} + * @param params - parameters, as per {@link DecryptionAlgorithm} */ class OlmDecryption extends DecryptionAlgorithm { /** - * @inheritdoc - * - * @param {MatrixEvent} event - * * returns a promise which resolves to a - * {@link module:crypto~EventDecryptionResult} once we have finished + * {@link EventDecryptionResult} once we have finished * decrypting. Rejects with an `algorithms.DecryptionError` if there is a * problem decrypting the event. */ @@ -275,10 +258,10 @@ class OlmDecryption extends DecryptionAlgorithm { /** * Attempt to decrypt an Olm message * - * @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender - * @param {object} message message object, with 'type' and 'body' fields + * @param theirDeviceIdentityKey - Curve25519 identity key of the sender + * @param message - message object, with 'type' and 'body' fields * - * @return {string} payload, if decrypted successfully. + * @returns payload, if decrypted successfully. */ private decryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise { // This is a wrapper that serialises decryptions of prekey messages, because diff --git a/src/crypto/api.ts b/src/crypto/api.ts index f6487ca91..468cc9933 100644 --- a/src/crypto/api.ts +++ b/src/crypto/api.ts @@ -66,8 +66,7 @@ export interface IRecoveryKey { export interface ICreateSecretStorageOpts { /** * Function called to await a secret storage key creation flow. - * Returns: - * {Promise} Object with public key metadata, encoded private + * @returns Promise resolving to an object with public key metadata, encoded private * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. */ @@ -131,6 +130,7 @@ export interface IImportOpts { } export interface IImportRoomKeysOpts { + /** called with an object that has a "stage" param */ progressCallback?: (stage: IImportOpts) => void; untrusted?: boolean; source?: string; // TODO: Enum diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index f2160165b..fa90c6b78 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -15,8 +15,6 @@ limitations under the License. */ /** - * @module crypto/backup - * * Classes for dealing with key backup. */ @@ -93,7 +91,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; @@ -134,7 +132,7 @@ export class BackupManager { * * Throws an error if a problem is detected. * - * @param {IKeyBackupInfo} info the key backup info + * @param info - the key backup info */ public static checkBackupVersion(info: IKeyBackupInfo): void { const Algorithm = algorithmsByName[info.algorithm]; @@ -276,7 +274,7 @@ export class BackupManager { * Forces a re-check of the key backup and enables/disables it * as appropriate. * - * @return {Object} Object with backup info (as returned by + * @returns Object with backup info (as returned by * getKeyBackupVersion) in backupInfo and * trust information (as returned by isKeyBackupTrusted) * in trustInfo. @@ -309,15 +307,7 @@ export class BackupManager { /** * Check if the given backup info is trusted. * - * @param {IKeyBackupInfo} backupInfo key backup info dict from /room_keys/version - * @return {object} { - * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device - * sigs: [ - * valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation - * deviceId: [string], - * device: [DeviceInfo || null], - * ] - * } + * @param backupInfo - key backup info dict from /room_keys/version */ public async isKeyBackupTrusted(backupInfo?: IKeyBackupInfo): Promise { const ret = { @@ -432,7 +422,7 @@ export class BackupManager { * Schedules sending all keys waiting to be sent to the backup, if not already * scheduled. Retries if necessary. * - * @param maxDelay Maximum delay to wait in ms. 0 means no delay. + * @param maxDelay - Maximum delay to wait in ms. 0 means no delay. */ public async scheduleKeyBackupSend(maxDelay = 10000): Promise { if (this.sendingBackups) return; @@ -490,8 +480,8 @@ export class BackupManager { * Take some e2e keys waiting to be backed up and send them * to the backup. * - * @param {number} limit Maximum number of keys to back up - * @returns {number} Number of sessions backed up + * @param limit - Maximum number of keys to back up + * @returns Number of sessions backed up */ public async backupPendingKeys(limit: number): Promise { const sessions = await this.baseApis.crypto!.cryptoStore.getSessionsNeedingBackup(limit); @@ -573,7 +563,7 @@ export class BackupManager { /** * Marks all group sessions as needing to be backed up without scheduling * them to upload in the background. - * @returns {Promise} Resolves to the number of sessions now requiring a backup + * @returns Promise which resolves to the number of sessions now requiring a backup * (which will be equal to the number of sessions in the store). */ public async flagAllGroupSessionsForBackup(): Promise { @@ -599,7 +589,7 @@ export class BackupManager { /** * Counts the number of end to end session keys that are waiting to be backed up - * @returns {Promise} Resolves to the number of sessions requiring backup + * @returns Promise which resolves to the number of sessions requiring backup */ public countSessionsNeedingBackup(): Promise { return this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup(); @@ -663,7 +653,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 +778,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..1fbbd2800 100644 --- a/src/crypto/deviceinfo.ts +++ b/src/crypto/deviceinfo.ts @@ -16,10 +16,6 @@ limitations under the License. import { ISignatures } from "../@types/signed"; -/** - * @module crypto/deviceinfo - */ - export interface IDevice { keys: Record; algorithms: string[]; @@ -37,69 +33,57 @@ enum DeviceVerification { /** * Information about a user's device - * - * @constructor - * @alias module:crypto/deviceinfo - * - * @property {string} deviceId the ID of this device - * - * @property {string[]} algorithms list of algorithms supported by this device - * - * @property {Object.} keys a map from - * <key type>:<id> -> <base64-encoded key>> - * - * @property {module:crypto/deviceinfo.DeviceVerification} verified - * whether the device has been verified/blocked by the user - * - * @property {boolean} known - * whether the user knows of this device's existence (useful when warning - * the user that a user has added new devices) - * - * @property {Object} unsigned additional data from the homeserver - * - * @param {string} deviceId id of the device */ export class DeviceInfo { /** * rehydrate a DeviceInfo from the session store * - * @param {object} obj raw object from session store - * @param {string} deviceId id of the device + * @param obj - raw object from session store + * @param deviceId - id of the device * - * @return {module:crypto~DeviceInfo} new DeviceInfo + * @returns new DeviceInfo */ public static fromStorage(obj: Partial, deviceId: string): 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; } - /** - * @enum - */ public static DeviceVerification = { VERIFIED: DeviceVerification.Verified, UNVERIFIED: DeviceVerification.Unverified, BLOCKED: DeviceVerification.Blocked, }; + /** list of algorithms supported by this device */ public algorithms: string[] = []; + /** a map from `: -> ` */ public keys: Record = {}; + /** whether the device has been verified/blocked by the user */ public verified = DeviceVerification.Unverified; + /** + * whether the user knows of this device's existence + * (useful when warning the user that a user has added new devices) + */ public known = false; + /** additional data from the homeserver */ public unsigned: Record = {}; public signatures: ISignatures = {}; + /** + * @param deviceId - id of the device + */ public constructor(public readonly deviceId: string) {} /** * Prepare a DeviceInfo for JSON serialisation in the session store * - * @return {object} deviceinfo with non-serialised members removed + * @returns deviceinfo with non-serialised members removed */ public toStorage(): IDevice { return { @@ -115,7 +99,7 @@ export class DeviceInfo { /** * Get the fingerprint for this device (ie, the Ed25519 key) * - * @return {string} base64-encoded fingerprint of this device + * @returns base64-encoded fingerprint of this device */ public getFingerprint(): string { return this.keys["ed25519:" + this.deviceId]; @@ -124,7 +108,7 @@ export class DeviceInfo { /** * Get the identity key for this device (ie, the Curve25519 key) * - * @return {string} base64-encoded identity key of this device + * @returns base64-encoded identity key of this device */ public getIdentityKey(): string { return this.keys["curve25519:" + this.deviceId]; @@ -133,7 +117,7 @@ export class DeviceInfo { /** * Get the configured display name for this device, if any * - * @return {string?} displayname + * @returns displayname */ public getDisplayName(): string | null { return this.unsigned.device_display_name || null; @@ -142,7 +126,7 @@ export class DeviceInfo { /** * Returns true if this device is blocked * - * @return {Boolean} true if blocked + * @returns true if blocked */ public isBlocked(): boolean { return this.verified == DeviceVerification.Blocked; @@ -151,7 +135,7 @@ export class DeviceInfo { /** * Returns true if this device is verified * - * @return {Boolean} true if verified + * @returns true if verified */ public isVerified(): boolean { return this.verified == DeviceVerification.Verified; @@ -160,7 +144,7 @@ export class DeviceInfo { /** * Returns true if this device is unverified * - * @return {Boolean} true if unverified + * @returns true if unverified */ public isUnverified(): boolean { return this.verified == DeviceVerification.Unverified; @@ -169,7 +153,7 @@ export class DeviceInfo { /** * Returns true if the user knows about this device's existence * - * @return {Boolean} true if known + * @returns true if known */ public isKnown(): boolean { return this.known === true; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 4a7f73e9b..9191522da 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -17,14 +17,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module crypto - */ - import anotherjson from "another-json"; +import { v4 as uuidv4 } from "uuid"; import type { PkDecryption, PkSigning } from "@matrix-org/olm"; -import { EventType } from "../@types/event"; +import { EventType, ToDeviceMessageId } from "../@types/event"; import { TypedReEmitter } from '../ReEmitter'; import { logger } from '../logger'; import { IExportedDevice, OlmDevice } from "./OlmDevice"; @@ -86,6 +83,10 @@ import { ISyncStateData } from "../sync"; 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; @@ -98,7 +99,7 @@ const defaultVerificationMethods = { // to start. [SHOW_QR_CODE_METHOD]: IllegalMethod, [SCAN_QR_CODE_METHOD]: IllegalMethod, -}; +} as const; /** * verification method names @@ -107,7 +108,7 @@ const defaultVerificationMethods = { export const verificationMethods = { RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME, SAS: SASVerification.NAME, -}; +} as const; export type VerificationMethod = keyof typeof verificationMethods | string; @@ -123,7 +124,12 @@ interface IInitOpts { } export interface IBootstrapCrossSigningOpts { + /** Optional. Reset even if keys already exist. */ setupNewCrossSigning?: boolean; + /** + * A function that makes the request requiring auth. Receives the auth data as an object. + * Can be called multiple times, first with an empty authDict, to obtain the flows. + */ authUploadDeviceSigningKeys?(makeRequest: (authData: any) => Promise<{}>): Promise; } @@ -156,18 +162,32 @@ interface IRoomKey { algorithm: string; } +/** + * The parameters of a room key request. The details of the request may + * vary with the crypto algorithm, but the management and storage layers for + * outgoing requests expect it to have 'room_id' and 'session_id' properties. + */ export interface IRoomKeyRequestBody extends IRoomKey { session_id: string; sender_key: string; } -export interface IMegolmSessionData { - [key: string]: any; // extensible +interface Extensible { + [key: string]: any; +} + +export interface IMegolmSessionData extends Extensible { + // Sender's Curve25519 device key sender_key: string; + // Devices which forwarded this session to us (normally empty). forwarding_curve25519_key_chain: string[]; + // Other keys the sender claims. sender_claimed_keys: Record; + // Room this session is used in room_id: string; + // Unique id for the session session_id: string; + // Base64'ed key data session_key: string; algorithm?: string; untrusted?: boolean; @@ -183,13 +203,6 @@ export interface ICheckOwnCrossSigningTrustOpts { allowPrivateKeyRequests?: boolean; } -/** - * @typedef {Object} module:crypto~OlmSessionResult - * @property {module:crypto/deviceinfo} device device info - * @property {string?} sessionId base64 olm session id; null if no session - * could be established - */ - interface IUserOlmSession { deviceIdKey: string; sessions: { @@ -198,25 +211,36 @@ interface IUserOlmSession { }[]; } -interface ISyncDeviceLists { - changed: string[]; - left: string[]; -} - export interface IRoomKeyRequestRecipient { userId: string; deviceId: string; } interface ISignableObject { - signatures?: object; + signatures?: ISignatures; unsigned?: object; } +/** + * The result of a (successful) call to decryptEvent. + */ export interface IEventDecryptionResult { + /** + * The plaintext payload for the event (typically containing type and content fields). + */ clearEvent: IClearEvent; + /** + * List of curve25519 keys involved in telling us about the senderCurve25519Key and claimedEd25519Key. + * See {@link MatrixEvent#getForwardingCurve25519KeyChain}. + */ forwardingCurve25519KeyChain?: string[]; + /** + * Key owned by the sender of this event. See {@link MatrixEvent#getSenderKey}. + */ senderCurve25519Key?: string; + /** + * ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}. + */ claimedEd25519Key?: string; untrusted?: boolean; } @@ -229,13 +253,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; + 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", @@ -254,10 +290,50 @@ export enum CryptoEvent { } export type CryptoEventHandlerMap = { + /** + * Fires when a device is marked as verified/unverified/blocked/unblocked by + * {@link MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or + * {@link MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}. + * + * @param userId - the owner of the verified device + * @param deviceId - the id of the verified device + * @param deviceInfo - updated device information + */ [CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, device: DeviceInfo) => void; + /** + * Fires when the trust status of a user changes + * If userId is the userId of the logged-in user, this indicated a change + * in the trust status of the cross-signing data on the account. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * @experimental + * + * @param userId - the userId of the user in question + * @param trustLevel - The new trust level of the user + */ [CryptoEvent.UserTrustStatusChanged]: (userId: string, trustLevel: UserTrustLevel) => void; + /** + * Fires when we receive a room key request + * + * @param req - request details + */ [CryptoEvent.RoomKeyRequest]: (request: IncomingRoomKeyRequest) => void; + /** + * Fires when we receive a room key request cancellation + */ [CryptoEvent.RoomKeyRequestCancellation]: (request: IncomingRoomKeyRequestCancellation) => void; + /** + * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() + * @param enabled - true if key backup has been enabled, otherwise false + * @example + * ``` + * matrixClient.on("crypto.keyBackupStatus", function(enabled){ + * if (enabled) { + * [...] + * } + * }); + * ``` + */ [CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void; [CryptoEvent.KeyBackupFailed]: (errcode: string) => void; [CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void; @@ -265,18 +341,50 @@ export type CryptoEventHandlerMap = { failures: IUploadKeySignaturesResponse["failures"], source: "checkOwnCrossSigningTrust" | "afterCrossSigningLocalKeyChange" | "setDeviceVerification", upload: (opts: { shouldEmit: boolean }) => Promise - ) => void; + ) => void;/** + * Fires when a key verification is requested. + */ [CryptoEvent.VerificationRequest]: (request: VerificationRequest) => void; + /** + * Fires when the app may wish to warn the user about something related + * the end-to-end crypto. + * + * @param type - One of the strings listed above + */ [CryptoEvent.Warning]: (type: string) => void; + /** + * Fires when the user's cross-signing keys have changed or cross-signing + * has been enabled/disabled. The client can use getStoredCrossSigningForUser + * with the user ID of the logged in user to check if cross-signing is + * enabled on the account. If enabled, it can test whether the current key + * is trusted using with checkUserTrust with the user ID of the logged + * in user. The checkOwnCrossSigningTrust function may be used to reconcile + * the trust in the account key. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * @experimental + */ [CryptoEvent.KeysChanged]: (data: {}) => void; + /** + * Fires whenever the stored devices for a user will be updated + * @param users - A list of user IDs that will be updated + * @param initialFetch - If true, the store is empty (apart + * from our own device) and is being seeded. + */ [CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void; + /** + * Fires whenever the stored devices for a user have changed + * @param users - A list of user IDs that were updated + * @param initialFetch - If true, the store was empty (apart + * from our own device) and has been seeded. + */ [CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void; [CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void; }; export class Crypto extends TypedEventEmitter { /** - * @return {string} The version of Olm. + * @returns The version of Olm. */ public static getOlmVersion(): [number, number, number] { return OlmDevice.getOlmVersion(); @@ -350,25 +458,21 @@ export class Crypto extends TypedEventEmitter, + verificationMethods: Array, ) { super(); this.reEmitter = new TypedReEmitter(this); @@ -476,8 +580,7 @@ export class Crypto extends TypedEventEmitter { @@ -544,7 +647,7 @@ export class Crypto extends TypedEventEmitter} Object with public key metadata, encoded private + * @returns Object with public key metadata, encoded private * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. */ @@ -615,6 +718,19 @@ export class Crypto extends TypedEventEmitter { + await this.downloadKeys([this.userId]); + return this.deviceList.getStoredCrossSigningForUser(this.userId) !== null; + } + /** * Checks whether cross signing: * - is enabled on this account and trusted by this device @@ -627,7 +743,7 @@ export class Crypto extends TypedEventEmitter { const publicKeysOnDevice = this.crossSigningInfo.getId(); @@ -652,7 +768,7 @@ export class Crypto extends TypedEventEmitter { const secretStorageKeyInAccount = await this.secretStorage.hasKey(); @@ -682,12 +798,12 @@ export class Crypto extends TypedEventEmitter} Object with public key metadata, encoded private + * Returns a Promise which resolves to an object with public key metadata, encoded private * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. - * @param {object} [opts.keyBackupInfo] The current key backup object. If passed, + * @param keyBackupInfo - The current key backup object. If passed, * the passphrase and recovery key from this backup will be used. - * @param {boolean} [opts.setupNewKeyBackup] If true, a new key backup version will be + * @param setupNewKeyBackup - If true, a new key backup version will be * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo * is supplied. - * @param {boolean} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist. - * @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's + * @param setupNewSecretStorage - Optional. Reset even if keys already exist. + * @param getKeyBackupPassphrase - Optional. Function called to get the user's * current key backup passphrase. Should return a promise that resolves with a Buffer * containing the key, or rejects if the key cannot be obtained. * Returns: - * {Promise} A promise which resolves to key creation data for + * A promise which resolves to key creation data for * SecretStorage#addKey: an object with `passphrase` etc fields. */ // TODO this does not resolve with what it says it does @@ -1138,9 +1253,9 @@ export class Crypto extends TypedEventEmitter { let key = await new Promise((resolve) => { // TODO types @@ -1188,8 +1303,8 @@ export class Crypto extends TypedEventEmitter): Promise { if (!(key instanceof Uint8Array)) { @@ -1211,9 +1326,9 @@ export class Crypto extends TypedEventEmitter | null): Promise { if (keys) { @@ -1720,7 +1835,7 @@ export class Crypto extends TypedEventEmitter { const shouldUpgradeCb = ( @@ -1762,7 +1877,7 @@ export class Crypto extends TypedEventEmitter { const deviceKeys = { @@ -1854,7 +1969,7 @@ export class Crypto extends TypedEventEmitterdeviceId->{@link - * module:crypto/deviceinfo|DeviceInfo}. + * @returns A promise which resolves to a map `userId->deviceId->{@link DeviceInfo}`. */ public downloadKeys(userIds: string[], forceDownload?: boolean): Promise { return this.deviceList.downloadKeys(userIds, !!forceDownload); @@ -2057,9 +2171,9 @@ export class Crypto extends TypedEventEmitter | null { @@ -2069,10 +2183,8 @@ export class Crypto extends TypedEventEmitter} true if the data was saved, false if + * @returns true if the data was saved, false if * it was not (eg. because no changes were pending). The promise * will only resolve once the data is saved, so may take some time * to resolve. @@ -2098,24 +2210,24 @@ export class Crypto extends TypedEventEmitter} keys The list of keys that was present + * @param keys - The list of keys that was present * during the device verification. This will be double checked with the list * of keys the given device has currently. * - * @return {Promise} updated DeviceInfo + * @returns updated DeviceInfo */ public async setDeviceVerification( userId: string, @@ -2269,6 +2381,9 @@ export class Crypto extends TypedEventEmitter * This method is provided for debugging purposes. * - * @param {string} userId id of user to inspect - * - * @return {Promise>} + * @param userId - id of user to inspect */ public async getOlmSessionsForUser(userId: string): Promise> { const devices = this.getStoredDevicesForUser(userId) || []; @@ -2403,9 +2516,7 @@ export class Crypto extends TypedEventEmitter { + const room = this.clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to enable encryption tracking devices in unknown room ${roomId}`); + } + await this.setRoomEncryptionImpl(room, config); + if (!this.lazyLoadMembers && !inhibitDeviceQuery) { + this.deviceList.refreshOutdatedDeviceLists(); + } + } + + /** + * Set up encryption for a room. + * + * This is called when an m.room.encryption event is received. It saves a flag + * for the room in the cryptoStore (if it wasn't already set), sets up an "encryptor" for + * the room, and enables device-list tracking for the room. + * + * It does not initiate a device list query for the room. That is normally + * done once we finish processing the sync, in onSyncCompleted. + * + * @param room - The room to enable encryption in. + * @param config - The encryption config for the room. + */ + private async setRoomEncryptionImpl( + room: Room, + config: IRoomEncryption, + ): Promise { + const roomId = room.roomId; + // ignore crypto events with no algorithm defined // This will happen if a crypto event is redacted before we fetch the room state // It would otherwise just throw later as an unknown algorithm would, but we may @@ -2625,14 +2769,7 @@ export class Crypto extends TypedEventEmitter { + const room = this.clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); + } + return this.trackRoomDevicesImpl(room); + } + + /** + * Make sure we are tracking the device lists for all users in this room. + * + * This is normally called when we are about to send an encrypted event, to make sure + * we have all the devices in the room; but it is also called when processing an + * m.room.encryption state event (if lazy-loading is disabled), or when members are + * loaded (if lazy-loading is enabled), to prepare the device list. + * + * @param room - Room to enable device-list tracking in + */ + private trackRoomDevicesImpl(room: Room): Promise { + const roomId = room.roomId; const trackMembers = async (): Promise => { // not an encrypted room if (!this.roomEncryptors.has(roomId)) { return; } - const room = this.clientStore.getRoom(roomId); - if (!room) { - throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); - } logger.log(`Starting to track devices for room ${roomId} ...`); const members = await room.getEncryptionTargetMembers(); members.forEach((m) => { @@ -2676,12 +2828,12 @@ export class Crypto extends TypedEventEmitter { const exportedSessions: IMegolmSessionData[] = []; @@ -2737,10 +2889,8 @@ export class Crypto extends TypedEventEmitter { let successes = 0; @@ -2774,7 +2924,7 @@ export class Crypto extends TypedEventEmitter} Resolves to the number of sessions requiring backup + * @returns Promise which resolves to the number of sessions requiring backup */ public countSessionsNeedingBackup(): Promise { return this.backupManager.countSessionsNeedingBackup(); @@ -2784,7 +2934,7 @@ export class Crypto extends TypedEventEmitter { @@ -2815,17 +2965,14 @@ export class Crypto extends TypedEventEmitter} resolves once we have + * @returns resolves once we have * finished decrypting. Rejects with an `algorithms.DecryptionError` if there * is a problem decrypting the event. */ @@ -2899,11 +3045,14 @@ 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; @@ -2922,12 +3071,10 @@ export class Crypto extends TypedEventEmitter} recipients - * @param {boolean} resend whether to resend the key request if there is + * @param resend - whether to resend the key request if there is * already one * - * @return {Promise} a promise that resolves when the key request is queued + * @returns a promise that resolves when the key request is queued */ public requestRoomKey( requestBody: IRoomKeyRequestBody, @@ -2951,8 +3098,7 @@ export class Crypto extends TypedEventEmitter { await this.outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests(); @@ -2972,25 +3118,18 @@ export class Crypto extends TypedEventEmitter { - const roomId = event.getRoomId()!; + public async onCryptoEvent(room: Room, event: MatrixEvent): Promise { const content = event.getContent(); - - try { - // inhibit the device list refresh for now - it will happen once we've - // finished processing the sync, in onSyncCompleted. - await this.setRoomEncryption(roomId, content, true); - } catch (e) { - logger.error(`Error configuring encryption in room ${roomId}`, e); - } + await this.setRoomEncryptionImpl(room, content); } /** * Called before the result of a sync is processed * - * @param {Object} syncData the data from the 'MatrixClient.sync' event + * @param syncData - the data from the 'MatrixClient.sync' event */ public async onSyncWillProcess(syncData: ISyncStateData): Promise { if (!syncData.oldSyncToken) { @@ -3014,7 +3153,7 @@ export class Crypto extends TypedEventEmitter { this.deviceList.setSyncToken(syncData.nextSyncToken ?? null); @@ -3047,18 +3186,17 @@ 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. @@ -3076,7 +3214,7 @@ export class Crypto extends TypedEventEmitter { const e2eUserIds: string[] = []; @@ -3093,7 +3231,7 @@ export class Crypto extends TypedEventEmitter { @@ -3115,11 +3253,11 @@ export class Crypto extends TypedEventEmitter} Promise which + * @param userDeviceInfoArr - the devices to send to + * @param payload - fields to include in the encrypted payload + * @returns Promise which * resolves once the message has been encrypted and sent to the given - * userDeviceMap, and returns the { contentMap, deviceInfoByDeviceId } + * userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }` * of the successfully sent messages. */ public async encryptAndSendToDevices( @@ -3138,6 +3276,7 @@ export class Crypto extends TypedEventEmitter { try { - logger.log(`received to_device ${event.getType()} from: ` + - `${event.getSender()} id: ${event.getId()}`); + logger.log(`received to-device ${event.getType()} from: ` + + `${event.getSender()} id: ${event.getContent()[ToDeviceMessageId]}`); if (event.getType() == "m.room_key" || event.getType() == "m.forwarded_room_key") { @@ -3232,8 +3371,8 @@ export class Crypto extends TypedEventEmitter { const content = event.getWireContent(); @@ -3477,10 +3616,11 @@ export class Crypto extends TypedEventEmitter { if (this.processingRoomKeyRequests) { @@ -3619,7 +3759,6 @@ export class Crypto extends TypedEventEmitter { const userId = req.userId; @@ -3709,7 +3848,6 @@ export class Crypto extends TypedEventEmitter | undefined; @@ -3784,9 +3919,9 @@ export class Crypto extends TypedEventEmitter { + public async signObject(obj: T): Promise { const sigs = obj.signatures || {}; const unsigned = obj.unsigned; @@ -3824,8 +3959,8 @@ export class Crypto extends TypedEventEmitter void; public constructor(event: MatrixEvent) { @@ -3878,14 +4005,13 @@ export class IncomingRoomKeyRequest { /** * Represents a received m.room_key_request cancellation - * - * @property {string} userId user requesting the cancellation - * @property {string} deviceId device requesting the cancellation - * @property {string} requestId unique id for the request to be cancelled */ class IncomingRoomKeyRequestCancellation { + /** user requesting the cancellation */ public readonly userId: string; + /** device requesting the cancellation */ public readonly deviceId: string; + /** unique id for the request to be cancelled */ public readonly requestId: string; public constructor(event: MatrixEvent) { @@ -3896,46 +4022,3 @@ class IncomingRoomKeyRequestCancellation { this.requestId = content.request_id; } } - -/** - * The result of a (successful) call to decryptEvent. - * - * @typedef {Object} EventDecryptionResult - * - * @property {Object} clearEvent The plaintext payload for the event - * (typically containing type and content fields). - * - * @property {?string} senderCurve25519Key Key owned by the sender of this - * event. See {@link module:models/event.MatrixEvent#getSenderKey}. - * - * @property {?string} claimedEd25519Key ed25519 key claimed by the sender of - * this event. See - * {@link module:models/event.MatrixEvent#getClaimedEd25519Key}. - * - * @property {?Array} forwardingCurve25519KeyChain list of curve25519 - * keys involved in telling us about the senderCurve25519Key and - * claimedEd25519Key. See - * {@link module:models/event.MatrixEvent#getForwardingCurve25519KeyChain}. - */ - -/** - * Fires when we receive a room key request - * - * @event module:client~MatrixClient#"crypto.roomKeyRequest" - * @param {module:crypto~IncomingRoomKeyRequest} req request details - */ - -/** - * Fires when we receive a room key request cancellation - * - * @event module:client~MatrixClient#"crypto.roomKeyRequestCancellation" - * @param {module:crypto~IncomingRoomKeyRequestCancellation} req - */ - -/** - * Fires when the app may wish to warn the user about something related - * the end-to-end crypto. - * - * @event module:client~MatrixClient#"crypto.warning" - * @param {string} type One of the strings listed above - */ diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts index 919266a3e..67e213c4a 100644 --- a/src/crypto/keybackup.ts +++ b/src/crypto/keybackup.ts @@ -59,6 +59,10 @@ export interface IKeyBackupInfo { /* eslint-enable camelcase */ export interface IKeyBackupPrepareOpts { + /** + * Whether to use Secure Secret Storage to store the key encrypting key backups. + * Optional, defaults to false. + */ secureSecretStorage: boolean; } diff --git a/src/crypto/olmlib.ts b/src/crypto/olmlib.ts index 111e4c16d..4a228e281 100644 --- a/src/crypto/olmlib.ts +++ b/src/crypto/olmlib.ts @@ -15,8 +15,6 @@ limitations under the License. */ /** - * @module olmlib - * * Utilities common to olm encryption algorithms */ @@ -31,6 +29,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", @@ -54,28 +53,26 @@ export const MEGOLM_ALGORITHM = Algorithm.Megolm; export const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup; export interface IOlmSessionResult { + /** device info */ device: DeviceInfo; + /** base64 olm session id; null if no session could be established */ sessionId: string | null; } /** * Encrypt an event payload for an Olm device * - * @param {Object} resultsObject The `ciphertext` property + * @param resultsObject - The `ciphertext` property * of the m.room.encrypted event to which to add our result * - * @param {string} ourUserId - * @param {string} ourDeviceId - * @param {module:crypto/OlmDevice} olmDevice olm.js wrapper - * @param {string} recipientUserId - * @param {module:crypto/deviceinfo} recipientDevice - * @param {object} payloadFields fields to include in the encrypted payload + * @param olmDevice - olm.js wrapper + * @param payloadFields - fields to include in the encrypted payload * * Returns a promise which resolves (to undefined) when the payload * has been encrypted into `resultsObject` */ export async function encryptMessageForDevice( - resultsObject: Record, + resultsObject: Record, ourUserId: string, ourDeviceId: string | undefined, olmDevice: OlmDevice, @@ -124,6 +121,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 +129,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 { @@ -147,17 +141,14 @@ interface IExistingOlmSession { * Get the existing olm sessions for the given devices, and the devices that * don't have olm sessions. * - * @param {module:crypto/OlmDevice} olmDevice * - * @param {MatrixClient} baseApis * - * @param {object} devicesByUser - * map from userid to list of devices to ensure sessions for + * @param devicesByUser - map from userid to list of devices to ensure sessions for * - * @return {Promise} resolves to an array. The first element of the array is a + * @returns resolves to an array. The first element of the array is a * a map of user IDs to arrays of deviceInfo, representing the devices that * don't have established olm sessions. The second element of the array is - * a map from userId to deviceId to {@link module:crypto~OlmSessionResult} + * a map from userId to deviceId to {@link OlmSessionResult} */ export async function getExistingOlmSessions( olmDevice: OlmDevice, @@ -199,27 +190,22 @@ export async function getExistingOlmSessions( /** * Try to make sure we have established olm sessions for the given devices. * - * @param {module:crypto/OlmDevice} olmDevice + * @param devicesByUser - map from userid to list of devices to ensure sessions for * - * @param {MatrixClient} baseApis - * - * @param {object} devicesByUser - * map from userid to list of devices to ensure sessions for - * - * @param {boolean} [force=false] If true, establish a new session even if one + * @param force - If true, establish a new session even if one * already exists. * - * @param {Number} [otkTimeout] The timeout in milliseconds when requesting + * @param otkTimeout - The timeout in milliseconds when requesting * one-time keys for establishing new olm sessions. * - * @param {Array} [failedServers] An array to fill with remote servers that + * @param failedServers - An array to fill with remote servers that * failed to respond to one-time-key requests. * - * @param {Logger} [log] A possibly customised log + * @param log - A possibly customised log * - * @return {Promise} resolves once the sessions are complete, to + * @returns resolves once the sessions are complete, to * an Object mapping from userId to deviceId to - * {@link module:crypto~OlmSessionResult} + * {@link OlmSessionResult} */ export async function ensureOlmSessionsForDevices( olmDevice: OlmDevice, @@ -444,15 +430,15 @@ export interface IObject { /** * Verify the signature on an object * - * @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op + * @param olmDevice - olm wrapper to use for verify op * - * @param {Object} obj object to check signature on. + * @param obj - object to check signature on. * - * @param {string} signingUserId ID of the user whose signature should be checked + * @param signingUserId - ID of the user whose signature should be checked * - * @param {string} signingDeviceId ID of the device whose signature should be checked + * @param signingDeviceId - ID of the device whose signature should be checked * - * @param {string} signingKey base64-ed ed25519 public key + * @param signingKey - base64-ed ed25519 public key * * Returns a promise which resolves (to undefined) if the the signature is good, * or rejects with an Error if it is bad. @@ -487,15 +473,15 @@ export async function verifySignature( /** * Sign a JSON object using public key cryptography - * @param {Object} obj Object to sign. The object will be modified to include + * @param obj - Object to sign. The object will be modified to include * the new signature - * @param {Olm.PkSigning|Uint8Array} key the signing object or the private key + * @param key - the signing object or the private key * seed - * @param {string} userId The user ID who owns the signing key - * @param {string} pubKey The public key (ignored if key is a seed) - * @returns {string} the signature for the object + * @param userId - The user ID who owns the signing key + * @param pubKey - The public key (ignored if key is a seed) + * @returns 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(); @@ -523,9 +509,9 @@ export function pkSign(obj: IObject, key: PkSigning, userId: string, pubKey: str /** * Verify a signed JSON object - * @param {Object} obj Object to verify - * @param {string} pubKey The public key to use to verify - * @param {string} userId The user ID who signed the object + * @param obj - Object to verify + * @param pubKey - The public key to use to verify + * @param userId - The user ID who signed the object */ export function pkVerify(obj: IObject, pubKey: string, userId: string): void { const keyId = "ed25519:" + pubKey; @@ -565,8 +551,8 @@ export function isOlmEncrypted(event: MatrixEvent): boolean { /** * Encode a typed array of uint8 as base64. - * @param {Uint8Array} uint8Array The data to encode. - * @return {string} The base64. + * @param uint8Array - The data to encode. + * @returns The base64. */ export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string { return Buffer.from(uint8Array).toString("base64"); @@ -574,8 +560,8 @@ export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string { /** * Encode a typed array of uint8 as unpadded base64. - * @param {Uint8Array} uint8Array The data to encode. - * @return {string} The unpadded base64. + * @param uint8Array - The data to encode. + * @returns The unpadded base64. */ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string { return encodeBase64(uint8Array).replace(/=+$/g, ''); @@ -583,8 +569,8 @@ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): stri /** * Decode a base64 string to a typed array of uint8. - * @param {string} base64 The base64 to decode. - * @return {Uint8Array} The decoded data. + * @param base64 - The base64 to decode. + * @returns The decoded data. */ export function decodeBase64(base64: string): Uint8Array { return Buffer.from(base64, "base64"); diff --git a/src/crypto/store/base.ts b/src/crypto/store/base.ts index 061582227..31ece3b06 100644 --- a/src/crypto/store/base.ts +++ b/src/crypto/store/base.ts @@ -30,8 +30,6 @@ import { IEncryptedPayload } from "../aes"; /** * Internal module. Definitions for storage for the crypto module - * - * @module */ export interface SecretStorePrivateKeys { @@ -46,8 +44,6 @@ export interface SecretStorePrivateKeys { /** * Abstraction of things that can store data required for end-to-end encryption - * - * @interface CryptoStore */ export interface CryptoStore { startup(): Promise; @@ -69,7 +65,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( @@ -197,32 +193,29 @@ export interface IWithheld { /** * Represents an outgoing room key request - * - * @typedef {Object} OutgoingRoomKeyRequest - * - * @property {string} requestId unique id for this request. Used for both - * an id within the request for later pairing with a cancellation, and for - * the transaction id when sending the to_device messages to our local - * server. - * - * @property {string?} cancellationTxnId - * transaction id for the cancellation, if any - * - * @property {Array<{userId: string, deviceId: string}>} recipients - * list of recipients for the request - * - * @property {module:crypto~RoomKeyRequestBody} requestBody - * parameters for the request. - * - * @property {Number} state current state of this request (states are defined - * in {@link module:crypto/OutgoingRoomKeyRequestManager~ROOM_KEY_REQUEST_STATES}) */ export interface OutgoingRoomKeyRequest { + /** + * Unique id for this request. Used for both an id within the request for later pairing with a cancellation, + * and for the transaction id when sending the to_device messages to our local server. + */ requestId: string; requestTxnId?: string; + /** + * Transaction id for the cancellation, if any + */ cancellationTxnId?: string; + /** + * List of recipients for the request + */ recipients: IRoomKeyRequestRecipient[]; + /** + * Parameters for the request + */ requestBody: IRoomKeyRequestBody; + /** + * current state of this request (states are defined in {@link OutgoingRoomKeyRequestManager}) + */ state: RoomKeyRequestState; } diff --git a/src/crypto/store/indexeddb-crypto-store-backend.ts b/src/crypto/store/indexeddb-crypto-store-backend.ts index cdf35e787..3a0c7711a 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.ts +++ b/src/crypto/store/indexeddb-crypto-store-backend.ts @@ -39,14 +39,11 @@ const PROFILE_TRANSACTIONS = false; * Implementation of a CryptoStore which is backed by an existing * IndexedDB connection. Generally you want IndexedDBCryptoStore * which connects to the database and defers to one of these. - * - * @implements {module:crypto/store/base~CryptoStore} */ export class Backend implements CryptoStore { private nextTxnId = 0; /** - * @param {IDBDatabase} db */ public constructor(private db: IDBDatabase) { // make sure we close the db on `onversionchange` - otherwise @@ -71,10 +68,9 @@ export class Backend implements CryptoStore { * Look for an existing outgoing room key request, and if none is found, * add a new one * - * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * @returns resolves to + * {@link OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise { @@ -113,11 +109,10 @@ export class Backend implements CryptoStore { /** * Look for an existing room key request * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {Promise} resolves to the matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the matching + * {@link OutgoingRoomKeyRequest}, or null if * not found */ public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { @@ -134,13 +129,12 @@ export class Backend implements CryptoStore { /** * look for an existing room key request in the db * - * @private - * @param {IDBTransaction} txn database transaction - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for - * @param {Function} callback function to call with the results of the + * @internal + * @param txn - database transaction + * @param requestBody - existing request to look for + * @param callback - function to call with the results of the * search. Either passed a matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * {@link OutgoingRoomKeyRequest}, or null if * not found. */ // eslint-disable-next-line @typescript-eslint/naming-convention @@ -181,10 +175,10 @@ export class Backend implements CryptoStore { /** * Look for room key requests by state * - * @param {Array} wantedStates list of acceptable states + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to the a - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the a + * {@link OutgoingRoomKeyRequest}, or null if * there are no pending requests in those states. If there are multiple * requests in those states, an arbitrary one is chosen. */ @@ -233,8 +227,7 @@ export class Backend implements CryptoStore { /** * - * @param {Number} wantedState - * @return {Promise>} All elements in a given state + * @returns All elements in a given state */ public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise { return new Promise((resolve, reject) => { @@ -294,12 +287,12 @@ export class Backend implements CryptoStore { * Look for an existing room key request by id and state, and update it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in - * @param {Object} updates name/value map of updates to apply + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in + * @param updates - name/value map of updates to apply * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to + * {@link OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ public updateOutgoingRoomKeyRequest( @@ -337,10 +330,10 @@ export class Backend implements CryptoStore { * Look for an existing room key request by id and state, and delete it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in * - * @returns {Promise} resolves once the operation is completed + * @returns resolves once the operation is completed */ public deleteOutgoingRoomKeyRequest( requestId: string, diff --git a/src/crypto/store/indexeddb-crypto-store.ts b/src/crypto/store/indexeddb-crypto-store.ts index d743a95de..ac2964d12 100644 --- a/src/crypto/store/indexeddb-crypto-store.ts +++ b/src/crypto/store/indexeddb-crypto-store.ts @@ -39,15 +39,11 @@ import { InboundGroupSessionData } from "../OlmDevice"; /** * Internal module. indexeddb storage for e2e. - * - * @module */ /** * An implementation of CryptoStore, which is normally backed by an indexeddb, * but with fallback to MemoryCryptoStore. - * - * @implements {module:crypto/store/base~CryptoStore} */ export class IndexedDBCryptoStore implements CryptoStore { public static STORE_ACCOUNT = 'account'; @@ -70,8 +66,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Create a new IndexedDBCryptoStore * - * @param {IDBFactory} indexedDB global indexedDB instance - * @param {string} dbName name of db to connect to + * @param indexedDB - global indexedDB instance + * @param dbName - name of db to connect to */ public constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {} @@ -81,7 +77,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * * This must be called before the store can be used. * - * @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend, + * @returns resolves to either an IndexedDBCryptoStoreBackend.Backend, * or a MemoryCryptoStore */ public startup(): Promise { @@ -167,7 +163,7 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Delete all data from this store. * - * @returns {Promise} resolves when the store has been cleared. + * @returns resolves when the store has been cleared. */ public deleteAllData(): Promise { return new Promise((resolve, reject) => { @@ -206,10 +202,9 @@ export class IndexedDBCryptoStore implements CryptoStore { * Look for an existing outgoing room key request, and if none is found, * add a new one * - * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * @returns resolves to + * {@link OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise { @@ -219,11 +214,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Look for an existing room key request * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {Promise} resolves to the matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the matching + * {@link OutgoingRoomKeyRequest}, or null if * not found */ public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { @@ -233,10 +227,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Look for room key requests by state * - * @param {Array} wantedStates list of acceptable states + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to the a - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the a + * {@link OutgoingRoomKeyRequest}, or null if * there are no pending requests in those states. If there are multiple * requests in those states, an arbitrary one is chosen. */ @@ -248,8 +242,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * Look for room key requests by state – * unlike above, return a list of all entries in one state. * - * @param {Number} wantedState - * @return {Promise>} Returns an array of requests in the given state + * @returns Returns an array of requests in the given state */ public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise { return this.backend!.getAllOutgoingRoomKeyRequestsByState(wantedState); @@ -258,12 +251,12 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Look for room key requests by target device and state * - * @param {string} userId Target user ID - * @param {string} deviceId Target device ID - * @param {Array} wantedStates list of acceptable states + * @param userId - Target user ID + * @param deviceId - Target device ID + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to a list of all the - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to a list of all the + * {@link OutgoingRoomKeyRequest} */ public getOutgoingRoomKeyRequestsByTarget( userId: string, @@ -279,12 +272,12 @@ export class IndexedDBCryptoStore implements CryptoStore { * Look for an existing room key request by id and state, and update it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in - * @param {Object} updates name/value map of updates to apply + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in + * @param updates - name/value map of updates to apply * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to + * {@link OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ public updateOutgoingRoomKeyRequest( @@ -301,10 +294,10 @@ export class IndexedDBCryptoStore implements CryptoStore { * Look for an existing room key request by id and state, and delete it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in * - * @returns {Promise} resolves once the operation is completed + * @returns resolves once the operation is completed */ public deleteOutgoingRoomKeyRequest( requestId: string, @@ -319,8 +312,8 @@ export class IndexedDBCryptoStore implements CryptoStore { * Get the account pickle from the store. * This requires an active transaction. See doTxn(). * - * @param {*} txn An active transaction. See doTxn(). - * @param {function(string)} func Called with the account pickle + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the account pickle */ public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void { this.backend!.getAccount(txn, func); @@ -330,8 +323,8 @@ export class IndexedDBCryptoStore implements CryptoStore { * Write the account pickle to the store. * This requires an active transaction. See doTxn(). * - * @param {*} txn An active transaction. See doTxn(). - * @param {string} accountPickle The new account pickle to store. + * @param txn - An active transaction. See doTxn(). + * @param accountPickle - The new account pickle to store. */ public storeAccount(txn: IDBTransaction, accountPickle: string): void { this.backend!.storeAccount(txn, accountPickle); @@ -341,9 +334,9 @@ export class IndexedDBCryptoStore implements CryptoStore { * Get the public part of the cross-signing keys (eg. self-signing key, * user signing key). * - * @param {*} txn An active transaction. See doTxn(). - * @param {function(string)} func Called with the account keys object: - * { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the account keys object: + * `{ key_type: base64 encoded seed }` where key type = user_signing_key_seed or self_signing_key_seed */ public getCrossSigningKeys( txn: IDBTransaction, @@ -353,9 +346,9 @@ export class IndexedDBCryptoStore implements CryptoStore { } /** - * @param {*} txn An active transaction. See doTxn(). - * @param {function(string)} func Called with the private key - * @param {string} type A key type + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the private key + * @param type - A key type */ public getSecretStorePrivateKey( txn: IDBTransaction, @@ -368,8 +361,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Write the cross-signing keys back to the store * - * @param {*} txn An active transaction. See doTxn(). - * @param {string} keys keys object as getCrossSigningKeys() + * @param txn - An active transaction. See doTxn(). + * @param keys - keys object as getCrossSigningKeys() */ public storeCrossSigningKeys(txn: IDBTransaction, keys: Record): void { this.backend!.storeCrossSigningKeys(txn, keys); @@ -378,9 +371,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Write the cross-signing private keys back to the store * - * @param {*} txn An active transaction. See doTxn(). - * @param {string} type The type of cross-signing private key to store - * @param {string} key keys object as getCrossSigningKeys() + * @param txn - An active transaction. See doTxn(). + * @param type - The type of cross-signing private key to store + * @param key - keys object as getCrossSigningKeys() */ public storeSecretStorePrivateKey( txn: IDBTransaction, @@ -394,8 +387,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Returns the number of end-to-end sessions in the store - * @param {*} txn An active transaction. See doTxn(). - * @param {function(int)} func Called with the count of sessions + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the count of sessions */ public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void { this.backend!.countEndToEndSessions(txn, func); @@ -404,10 +397,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Retrieve a specific end-to-end session between the logged-in user * and another device. - * @param {string} deviceKey The public key of the other device. - * @param {string} sessionId The ID of the session to retrieve - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called with A map from sessionId + * @param deviceKey - The public key of the other device. + * @param sessionId - The ID of the session to retrieve + * @param txn - An active transaction. See doTxn(). + * @param func - Called with A map from sessionId * to session information object with 'session' key being the * Base64 end-to-end session and lastReceivedMessageTs being the * timestamp in milliseconds at which the session last received @@ -425,9 +418,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Retrieve the end-to-end sessions between the logged-in user and another * device. - * @param {string} deviceKey The public key of the other device. - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called with A map from sessionId + * @param deviceKey - The public key of the other device. + * @param txn - An active transaction. See doTxn(). + * @param func - Called with A map from sessionId * to session information object with 'session' key being the * Base64 end-to-end session and lastReceivedMessageTs being the * timestamp in milliseconds at which the session last received @@ -443,8 +436,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Retrieve all end-to-end sessions - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called one for each session with + * @param txn - An active transaction. See doTxn(). + * @param func - Called one for each session with * an object with, deviceKey, lastReceivedMessageTs, sessionId * and session keys. */ @@ -454,10 +447,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Store a session between the logged-in user and another device - * @param {string} deviceKey The public key of the other device. - * @param {string} sessionId The ID for this end-to-end session. - * @param {string} sessionInfo Session information object - * @param {*} txn An active transaction. See doTxn(). + * @param deviceKey - The public key of the other device. + * @param sessionId - The ID for this end-to-end session. + * @param sessionInfo - Session information object + * @param txn - An active transaction. See doTxn(). */ public storeEndToEndSession( deviceKey: string, @@ -485,10 +478,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Retrieve the end-to-end inbound group session for a given * server key and session ID - * @param {string} senderCurve25519Key The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called with A map from sessionId + * @param senderCurve25519Key - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param txn - An active transaction. See doTxn(). + * @param func - Called with A map from sessionId * to Base64 end-to-end session. */ public getEndToEndInboundGroupSession( @@ -502,10 +495,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Fetches all inbound group sessions in the store - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called once for each group session - * in the store with an object having keys {senderKey, sessionId, - * sessionData}, then once with null to indicate the end of the list. + * @param txn - An active transaction. See doTxn(). + * @param func - Called once for each group session + * in the store with an object having keys `{senderKey, sessionId, sessionData}`, + * then once with null to indicate the end of the list. */ public getAllEndToEndInboundGroupSessions( txn: IDBTransaction, @@ -518,10 +511,10 @@ export class IndexedDBCryptoStore implements CryptoStore { * Adds an end-to-end inbound group session to the store. * If there already exists an inbound group session with the same * senderCurve25519Key and sessionID, the session will not be added. - * @param {string} senderCurve25519Key The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {object} sessionData The session data structure - * @param {*} txn An active transaction. See doTxn(). + * @param senderCurve25519Key - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param sessionData - The session data structure + * @param txn - An active transaction. See doTxn(). */ public addEndToEndInboundGroupSession( senderCurve25519Key: string, @@ -536,10 +529,10 @@ export class IndexedDBCryptoStore implements CryptoStore { * Writes an end-to-end inbound group session to the store. * If there already exists an inbound group session with the same * senderCurve25519Key and sessionID, it will be overwritten. - * @param {string} senderCurve25519Key The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {object} sessionData The session data structure - * @param {*} txn An active transaction. See doTxn(). + * @param senderCurve25519Key - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param sessionData - The session data structure + * @param txn - An active transaction. See doTxn(). */ public storeEndToEndInboundGroupSession( senderCurve25519Key: string, @@ -568,8 +561,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * These all need to be written out in full each time such that the snapshot * is always consistent, so they are stored in one object. * - * @param {Object} deviceData - * @param {*} txn An active transaction. See doTxn(). + * @param txn - An active transaction. See doTxn(). */ public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void { this.backend!.storeEndToEndDeviceData(deviceData, txn); @@ -578,8 +570,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Get the state of all tracked devices * - * @param {*} txn An active transaction. See doTxn(). - * @param {function(Object)} func Function called with the + * @param txn - An active transaction. See doTxn(). + * @param func - Function called with the * device data */ public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void { @@ -590,18 +582,18 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Store the end-to-end state for a room. - * @param {string} roomId The room's ID. - * @param {object} roomInfo The end-to-end info for the room. - * @param {*} txn An active transaction. See doTxn(). + * @param roomId - The room's ID. + * @param roomInfo - The end-to-end info for the room. + * @param txn - An active transaction. See doTxn(). */ public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void { this.backend!.storeEndToEndRoom(roomId, roomInfo, txn); } /** - * Get an object of roomId->roomInfo for all e2e rooms in the store - * @param {*} txn An active transaction. See doTxn(). - * @param {function(Object)} func Function called with the end to end encrypted rooms + * Get an object of `roomId->roomInfo` for all e2e rooms in the store + * @param txn - An active transaction. See doTxn(). + * @param func - Function called with the end-to-end encrypted rooms */ public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record) => void): void { this.backend!.getEndToEndRooms(txn, func); @@ -611,9 +603,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Get the inbound group sessions that need to be backed up. - * @param {number} limit The maximum number of sessions to retrieve. 0 + * @param limit - The maximum number of sessions to retrieve. 0 * for no limit. - * @returns {Promise} resolves to an array of inbound group sessions + * @returns resolves to an array of inbound group sessions */ public getSessionsNeedingBackup(limit: number): Promise { return this.backend!.getSessionsNeedingBackup(limit); @@ -621,8 +613,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Count the inbound group sessions that need to be backed up. - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} resolves to the number of sessions + * @param txn - An active transaction. See doTxn(). (optional) + * @returns resolves to the number of sessions */ public countSessionsNeedingBackup(txn?: IDBTransaction): Promise { return this.backend!.countSessionsNeedingBackup(txn); @@ -630,9 +622,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Unmark sessions as needing to be backed up. - * @param {Array} sessions The sessions that need to be backed up. - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} resolves when the sessions are unmarked + * @param sessions - The sessions that need to be backed up. + * @param txn - An active transaction. See doTxn(). (optional) + * @returns resolves when the sessions are unmarked */ public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise { return this.backend!.unmarkSessionsNeedingBackup(sessions, txn); @@ -640,9 +632,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Mark sessions as needing to be backed up. - * @param {Array} sessions The sessions that need to be backed up. - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} resolves when the sessions are marked + * @param sessions - The sessions that need to be backed up. + * @param txn - An active transaction. See doTxn(). (optional) + * @returns resolves when the sessions are marked */ public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise { return this.backend!.markSessionsNeedingBackup(sessions, txn); @@ -650,10 +642,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Add a shared-history group session for a room. - * @param {string} roomId The room that the key belongs to - * @param {string} senderKey The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {*} txn An active transaction. See doTxn(). (optional) + * @param roomId - The room that the key belongs to + * @param senderKey - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param txn - An active transaction. See doTxn(). (optional) */ public addSharedHistoryInboundGroupSession( roomId: string, @@ -666,9 +658,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Get the shared-history group session for a room. - * @param {string} roomId The room that the key belongs to - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} Resolves to an array of [senderKey, sessionId] + * @param roomId - The room that the key belongs to + * @param txn - An active transaction. See doTxn(). (optional) + * @returns Promise which resolves to an array of [senderKey, sessionId] */ public getSharedHistoryInboundGroupSessions( roomId: string, @@ -704,16 +696,16 @@ export class IndexedDBCryptoStore implements CryptoStore { * only be called within a callback of either this function or * one of the store functions operating on the same transaction. * - * @param {string} mode 'readwrite' if you need to call setter + * @param mode - 'readwrite' if you need to call setter * functions with this transaction. Otherwise, 'readonly'. - * @param {string[]} stores List IndexedDBCryptoStore.STORE_* + * @param stores - List IndexedDBCryptoStore.STORE_* * options representing all types of object that will be * accessed or written to with this transaction. - * @param {function(*)} func Function called with the + * @param func - Function called with the * transaction object: an opaque object that should be passed * to store functions. - * @param {Logger} [log] A possibly customised log - * @return {Promise} Promise that resolves with the result of the `func` + * @param log - A possibly customised log + * @returns Promise that resolves with the result of the `func` * when the transaction is complete. If the backend is * async (ie. the indexeddb backend) any of the callback * functions throwing an exception will cause this promise to diff --git a/src/crypto/store/localStorage-crypto-store.ts b/src/crypto/store/localStorage-crypto-store.ts index 977236ef9..04815cf15 100644 --- a/src/crypto/store/localStorage-crypto-store.ts +++ b/src/crypto/store/localStorage-crypto-store.ts @@ -28,8 +28,6 @@ import { InboundGroupSessionData } from "../OlmDevice"; * some things backed by localStorage. It exists because indexedDB * is broken in Firefox private mode or set to, "will not remember * history". - * - * @module */ const E2E_PREFIX = "crypto."; @@ -62,9 +60,6 @@ function keyEndToEndRoomsPrefix(roomId: string): string { return KEY_ROOMS_PREFIX + roomId; } -/** - * @implements {module:crypto/store/base~CryptoStore} - */ export class LocalStorageCryptoStore extends MemoryCryptoStore { public static exists(store: Storage): boolean { const length = store.length; @@ -338,20 +333,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; } @@ -364,7 +359,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { /** * Delete all data from this store. * - * @returns {Promise} Promise which resolves when the store has been cleared. + * @returns Promise which resolves when the store has been cleared. */ public deleteAllData(): Promise { this.store.removeItem(KEY_END_TO_END_ACCOUNT); diff --git a/src/crypto/store/memory-crypto-store.ts b/src/crypto/store/memory-crypto-store.ts index f22379ee8..701f85fc3 100644 --- a/src/crypto/store/memory-crypto-store.ts +++ b/src/crypto/store/memory-crypto-store.ts @@ -35,13 +35,8 @@ import { InboundGroupSessionData } from "../OlmDevice"; /** * Internal module. in-memory storage for e2e. - * - * @module */ -/** - * @implements {module:crypto/store/base~CryptoStore} - */ export class MemoryCryptoStore implements CryptoStore { private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = []; private account: string | null = null; @@ -65,7 +60,7 @@ export class MemoryCryptoStore implements CryptoStore { * * This must be called before the store can be used. * - * @return {Promise} resolves to the store. + * @returns resolves to the store. */ public async startup(): Promise { // No startup work to do for the memory store. @@ -75,7 +70,7 @@ export class MemoryCryptoStore implements CryptoStore { /** * Delete all data from this store. * - * @returns {Promise} Promise which resolves when the store has been cleared. + * @returns Promise which resolves when the store has been cleared. */ public deleteAllData(): Promise { return Promise.resolve(); @@ -85,10 +80,9 @@ export class MemoryCryptoStore implements CryptoStore { * Look for an existing outgoing room key request, and if none is found, * add a new one * - * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * @returns resolves to + * {@link OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise { @@ -122,11 +116,10 @@ export class MemoryCryptoStore implements CryptoStore { /** * Look for an existing room key request * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {Promise} resolves to the matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the matching + * {@link OutgoingRoomKeyRequest}, or null if * not found */ public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { @@ -138,10 +131,9 @@ export class MemoryCryptoStore implements CryptoStore { * * @internal * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {module:crypto/store/base~OutgoingRoomKeyRequest?} + * @returns * the matching request, or null if not found */ // eslint-disable-next-line @typescript-eslint/naming-convention @@ -157,10 +149,10 @@ export class MemoryCryptoStore implements CryptoStore { /** * Look for room key requests by state * - * @param {Array} wantedStates list of acceptable states + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to the a - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the a + * {@link OutgoingRoomKeyRequest}, or null if * there are no pending requests in those states */ public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise { @@ -176,8 +168,7 @@ export class MemoryCryptoStore implements CryptoStore { /** * - * @param {Number} wantedState - * @return {Promise>} All OutgoingRoomKeyRequests in state + * @returns All OutgoingRoomKeyRequests in state */ public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise { return Promise.resolve( @@ -210,12 +201,12 @@ export class MemoryCryptoStore implements CryptoStore { * Look for an existing room key request by id and state, and update it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in - * @param {Object} updates name/value map of updates to apply + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in + * @param updates - name/value map of updates to apply * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to + * {@link OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ public updateOutgoingRoomKeyRequest( @@ -246,10 +237,10 @@ export class MemoryCryptoStore implements CryptoStore { * Look for an existing room key request by id and state, and delete it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in * - * @returns {Promise} resolves once the operation is completed + * @returns resolves once the operation is completed */ public deleteOutgoingRoomKeyRequest( requestId: string, diff --git a/src/crypto/verification/Base.ts b/src/crypto/verification/Base.ts index 55b349e99..f2644b557 100644 --- a/src/crypto/verification/Base.ts +++ b/src/crypto/verification/Base.ts @@ -17,7 +17,6 @@ limitations under the License. /** * Base class for verification methods. - * @module crypto/verification/Base */ import { MatrixEvent } from '../../models/event'; @@ -74,21 +73,19 @@ export class VerificationBase< * *

Subclasses must have a NAME class property.

* - * @class - * - * @param {Object} channel the verification channel to send verification messages over. + * @param channel - the verification channel to send verification messages over. * TODO: Channel types * - * @param {MatrixClient} baseApis base matrix api interface + * @param baseApis - base matrix api interface * - * @param {string} userId the user ID that is being verified + * @param userId - the user ID that is being verified * - * @param {string} deviceId the device ID that is being verified + * @param deviceId - the device ID that is being verified * - * @param {object} [startEvent] the m.key.verification.start event that + * @param startEvent - the m.key.verification.start event that * initiated this verification, if any * - * @param {object} [request] the key verification request object related to + * @param request - the key verification request object related to * this verification, if any */ public constructor( @@ -279,7 +276,7 @@ export class VerificationBase< /** * Begin the key verification * - * @returns {Promise} Promise which resolves when the verification has + * @returns Promise which resolves when the verification has * completed. */ public verify(): Promise { diff --git a/src/crypto/verification/Error.ts b/src/crypto/verification/Error.ts index ca0fb20b5..0ad50b2ff 100644 --- a/src/crypto/verification/Error.ts +++ b/src/crypto/verification/Error.ts @@ -16,8 +16,6 @@ limitations under the License. /** * Error messages. - * - * @module crypto/verification/Error */ import { MatrixEvent } from "../../models/event"; diff --git a/src/crypto/verification/IllegalMethod.ts b/src/crypto/verification/IllegalMethod.ts index f01364a21..c437e0cd2 100644 --- a/src/crypto/verification/IllegalMethod.ts +++ b/src/crypto/verification/IllegalMethod.ts @@ -17,7 +17,6 @@ limitations under the License. /** * Verification method that is illegal to have (cannot possibly * do verification with this method). - * @module crypto/verification/IllegalMethod */ import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base"; @@ -26,10 +25,6 @@ import { MatrixClient } from "../../client"; import { MatrixEvent } from "../../models/event"; import { VerificationRequest } from "./request/VerificationRequest"; -/** - * @class crypto/verification/IllegalMethod/IllegalMethod - * @extends {module:crypto/verification/Base} - */ export class IllegalMethod extends Base { public static factory( channel: IVerificationChannel, diff --git a/src/crypto/verification/QRCode.ts b/src/crypto/verification/QRCode.ts index f6bdda17e..c5c2735bc 100644 --- a/src/crypto/verification/QRCode.ts +++ b/src/crypto/verification/QRCode.ts @@ -16,7 +16,6 @@ limitations under the License. /** * QR code key verification. - * @module crypto/verification/QRCode */ import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base"; @@ -44,10 +43,6 @@ type EventHandlerMap = { [QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void; } & VerificationEventHandlerMap; -/** - * @class crypto/verification/QRCode/ReciprocateQRCode - * @extends {module:crypto/verification/Base} - */ export class ReciprocateQRCode extends Base { public reciprocateQREvent?: IReciprocateQr; @@ -283,21 +278,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 +302,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..9d831f5c6 100644 --- a/src/crypto/verification/SAS.ts +++ b/src/crypto/verification/SAS.ts @@ -16,7 +16,6 @@ limitations under the License. /** * Short Authentication String (SAS) verification. - * @module crypto/verification/SAS */ import anotherjson from 'another-json'; @@ -140,7 +139,7 @@ function generateEmojiSas(sasBytes: number[]): EmojiMapping[] { const sasGenerators = { decimal: generateDecimalSas, emoji: generateEmojiSas, -}; +} as const; export interface IGeneratedSas { decimal?: [number, number, number]; @@ -154,11 +153,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 +168,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 +201,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); @@ -230,10 +231,6 @@ type EventHandlerMap = { [SasEvent.ShowSas]: (sas: ISasEvent) => void; } & VerificationEventHandlerMap; -/** - * @alias module:crypto/verification/SAS - * @extends {module:crypto/verification/Base} - */ export class SAS extends Base { private waitingForAccept?: boolean; public ourSASPubKey?: string; @@ -299,10 +296,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 +351,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 +442,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 +452,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 +474,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/crypto/verification/SASDecimal.ts b/src/crypto/verification/SASDecimal.ts index c8fa73100..26dc8d2a0 100644 --- a/src/crypto/verification/SASDecimal.ts +++ b/src/crypto/verification/SASDecimal.ts @@ -17,11 +17,11 @@ limitations under the License. /** * Implementation of decimal encoding of SAS as per: * https://spec.matrix.org/v1.4/client-server-api/#sas-method-decimal - * @param sasBytes the five bytes generated by HKDF + * @param sasBytes - the five bytes generated by HKDF * @returns the derived three numbers between 1000 and 9191 inclusive */ export function generateDecimalSas(sasBytes: number[]): [number, number, number] { - /** + /* * +--------+--------+--------+--------+--------+ * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | * +--------+--------+--------+--------+--------+ diff --git a/src/crypto/verification/request/InRoomChannel.ts b/src/crypto/verification/request/InRoomChannel.ts index 664a2dad6..edbf31f63 100644 --- a/src/crypto/verification/request/InRoomChannel.ts +++ b/src/crypto/verification/request/InRoomChannel.ts @@ -40,9 +40,9 @@ export class InRoomChannel implements IVerificationChannel { private requestEventId?: string; /** - * @param {MatrixClient} client the matrix client, to send messages with and get current user & device from. - * @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user. - * @param {string} userId id of user that the verification request is directed at, should be present in the room. + * @param client - the matrix client, to send messages with and get current user & device from. + * @param roomId - id of the room where verification events should be posted in, should be a DM with the given user. + * @param userId - id of user that the verification request is directed at, should be present in the room. */ public constructor( private readonly client: MatrixClient, @@ -78,8 +78,8 @@ export class InRoomChannel implements IVerificationChannel { } /** - * @param {MatrixEvent} event the event to get the timestamp of - * @return {number} the timestamp when the event was sent + * @param event - the event to get the timestamp of + * @returns the timestamp when the event was sent */ public getTimestamp(event: MatrixEvent): number { return event.getTs(); @@ -87,8 +87,8 @@ export class InRoomChannel implements IVerificationChannel { /** * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel - * @param {string} type the event type to check - * @returns {boolean} boolean flag + * @param type - the event type to check + * @returns boolean flag */ public static canCreateRequest(type: string): boolean { return type === REQUEST_TYPE; @@ -100,8 +100,8 @@ export class InRoomChannel implements IVerificationChannel { /** * Extract the transaction id used by a given key verification event, if any - * @param {MatrixEvent} event the event - * @returns {string} the transaction id + * @param event - the event + * @returns the transaction id */ public static getTransactionId(event: MatrixEvent): string | undefined { if (InRoomChannel.getEventType(event) === REQUEST_TYPE) { @@ -119,9 +119,9 @@ export class InRoomChannel implements IVerificationChannel { * This only does checks that don't rely on the current state of a potentially already channel * so we can prevent channels being created by invalid events. * `handleEvent` can do more checks and choose to ignore invalid events. - * @param {MatrixEvent} event the event to validate - * @param {MatrixClient} client the client to get the current user and device id from - * @returns {boolean} whether the event is valid and should be passed to handleEvent + * @param event - the event to validate + * @param client - the client to get the current user and device id from + * @returns whether the event is valid and should be passed to handleEvent */ public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean { const txnId = InRoomChannel.getTransactionId(event); @@ -156,8 +156,8 @@ export class InRoomChannel implements IVerificationChannel { * As m.key.verification.request events are as m.room.message events with the InRoomChannel * to have a fallback message in non-supporting clients, we map the real event type * to the symbolic one to keep things in unison with ToDeviceChannel - * @param {MatrixEvent} event the event to get the type of - * @returns {string} the "symbolic" event type + * @param event - the event to get the type of + * @returns the "symbolic" event type */ public static getEventType(event: MatrixEvent): string { const type = event.getType(); @@ -179,10 +179,10 @@ export class InRoomChannel implements IVerificationChannel { /** * Changes the state of the channel, request, and verifier in response to a key verification event. - * @param {MatrixEvent} event to handle - * @param {VerificationRequest} request the request to forward handling to - * @param {boolean} isLiveEvent whether this is an even received through sync or not - * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. + * @param event - to handle + * @param request - the request to forward handling to + * @param isLiveEvent - whether this is an even received through sync or not + * @returns a promise that resolves when any requests as an answer to the passed-in event are sent. */ public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise { // prevent processing the same event multiple times, as under @@ -228,8 +228,8 @@ export class InRoomChannel implements IVerificationChannel { * so it has the same format as returned by `completeContent` before sending. * The relation can not appear on the event content because of encryption, * relations are excluded from encryption. - * @param {MatrixEvent} event the received event - * @returns {Object} the content object with the relation added again + * @param event - the received event + * @returns the content object with the relation added again */ public completedContentFromEvent(event: MatrixEvent): Record { // ensure m.related_to is included in e2ee rooms @@ -244,9 +244,9 @@ export class InRoomChannel implements IVerificationChannel { * This is public so verification methods (SAS uses this) can get the exact * content that will be sent independent of the used channel, * as they need to calculate the hash of it. - * @param {string} type the event type - * @param {object} content the (incomplete) content - * @returns {object} the complete content, as it will be sent. + * @param type - the event type + * @param content - the (incomplete) content + * @returns the complete content, as it will be sent. */ public completeContent(type: string, content: Record): Record { content = Object.assign({}, content); @@ -276,9 +276,9 @@ export class InRoomChannel implements IVerificationChannel { /** * Send an event over the channel with the content not having gone through `completeContent`. - * @param {string} type the event type - * @param {object} uncompletedContent the (incomplete) content - * @returns {Promise} the promise of the request + * @param type - the event type + * @param uncompletedContent - the (incomplete) content + * @returns the promise of the request */ public send(type: string, uncompletedContent: Record): Promise { const content = this.completeContent(type, uncompletedContent); @@ -287,9 +287,8 @@ export class InRoomChannel implements IVerificationChannel { /** * Send an event over the channel with the content having gone through `completeContent` already. - * @param {string} type the event type - * @param {object} content - * @returns {Promise} the promise of the request + * @param type - the event type + * @returns the promise of the request */ public async sendCompleted(type: string, content: Record): Promise { let sendType = type; diff --git a/src/crypto/verification/request/ToDeviceChannel.ts b/src/crypto/verification/request/ToDeviceChannel.ts index 30d615251..5d92c3ed1 100644 --- a/src/crypto/verification/request/ToDeviceChannel.ts +++ b/src/crypto/verification/request/ToDeviceChannel.ts @@ -69,8 +69,8 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Extract the transaction id used by a given key verification event, if any - * @param {MatrixEvent} event the event - * @returns {string} the transaction id + * @param event - the event + * @returns the transaction id */ public static getTransactionId(event: MatrixEvent): string { const content = event.getContent(); @@ -79,8 +79,8 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel - * @param {string} type the event type to check - * @returns {boolean} boolean flag + * @param type - the event type to check + * @returns boolean flag */ public static canCreateRequest(type: string): boolean { return type === REQUEST_TYPE || type === START_TYPE; @@ -95,9 +95,9 @@ export class ToDeviceChannel implements IVerificationChannel { * This only does checks that don't rely on the current state of a potentially already channel * so we can prevent channels being created by invalid events. * `handleEvent` can do more checks and choose to ignore invalid events. - * @param {MatrixEvent} event the event to validate - * @param {MatrixClient} client the client to get the current user and device id from - * @returns {boolean} whether the event is valid and should be passed to handleEvent + * @param event - the event to validate + * @param client - the client to get the current user and device id from + * @returns whether the event is valid and should be passed to handleEvent */ public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean { if (event.isCancelled()) { @@ -137,8 +137,8 @@ export class ToDeviceChannel implements IVerificationChannel { } /** - * @param {MatrixEvent} event the event to get the timestamp of - * @return {number} the timestamp when the event was sent + * @param event - the event to get the timestamp of + * @returns the timestamp when the event was sent */ public getTimestamp(event: MatrixEvent): number { const content = event.getContent(); @@ -147,10 +147,10 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Changes the state of the channel, request, and verifier in response to a key verification event. - * @param {MatrixEvent} event to handle - * @param {VerificationRequest} request the request to forward handling to - * @param {boolean} isLiveEvent whether this is an even received through sync or not - * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. + * @param event - to handle + * @param request - the request to forward handling to + * @param isLiveEvent - whether this is an even received through sync or not + * @returns a promise that resolves when any requests as an answer to the passed-in event are sent. */ public async handleEvent(event: MatrixEvent, request: Request, isLiveEvent = false): Promise { const type = event.getType(); @@ -196,9 +196,9 @@ export class ToDeviceChannel implements IVerificationChannel { } /** - * See {InRoomChannel.completedContentFromEvent} why this is needed. - * @param {MatrixEvent} event the received event - * @returns {Object} the content object + * See {@link InRoomChannel#completedContentFromEvent} for why this is needed. + * @param event - the received event + * @returns the content object */ public completedContentFromEvent(event: MatrixEvent): Record { return event.getContent(); @@ -209,9 +209,9 @@ export class ToDeviceChannel implements IVerificationChannel { * This is public so verification methods (SAS uses this) can get the exact * content that will be sent independent of the used channel, * as they need to calculate the hash of it. - * @param {string} type the event type - * @param {object} content the (incomplete) content - * @returns {object} the complete content, as it will be sent. + * @param type - the event type + * @param content - the (incomplete) content + * @returns the complete content, as it will be sent. */ public completeContent(type: string, content: Record): Record { // make a copy @@ -230,9 +230,9 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Send an event over the channel with the content not having gone through `completeContent`. - * @param {string} type the event type - * @param {object} uncompletedContent the (incomplete) content - * @returns {Promise} the promise of the request + * @param type - the event type + * @param uncompletedContent - the (incomplete) content + * @returns the promise of the request */ public send(type: string, uncompletedContent: Record = {}): Promise { // create transaction id when sending request @@ -245,9 +245,8 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Send an event over the channel with the content having gone through `completeContent` already. - * @param {string} type the event type - * @param {object} content - * @returns {Promise} the promise of the request + * @param type - the event type + * @returns the promise of the request */ public async sendCompleted(type: string, content: Record): Promise { let result; @@ -286,7 +285,7 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Allow Crypto module to create and know the transaction id before the .start event gets sent. - * @returns {string} the transaction id + * @returns the transaction id */ public static makeTransactionId(): string { return randomString(32); diff --git a/src/crypto/verification/request/VerificationRequest.ts b/src/crypto/verification/request/VerificationRequest.ts index 58de4b9a2..05f0cd75c 100644 --- a/src/crypto/verification/request/VerificationRequest.ts +++ b/src/crypto/verification/request/VerificationRequest.ts @@ -81,6 +81,9 @@ export enum VerificationRequestEvent { } type EventHandlerMap = { + /** + * Fires whenever the state of the request object has changed. + */ [VerificationRequestEvent.Change]: () => void; }; @@ -88,7 +91,6 @@ type EventHandlerMap = { * State machine for verification requests. * Things that differ based on what channel is used to * send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`. - * @event "change" whenever the state of the request object has changed. */ export class VerificationRequest< C extends IVerificationChannel = IVerificationChannel, @@ -129,10 +131,10 @@ export class VerificationRequest< /** * Stateless validation logic not specific to the channel. * Invoked by the same static method in either channel. - * @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel. - * @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead. - * @param {MatrixClient} client the client to get the current user and device id from - * @returns {boolean} whether the event is valid and should be passed to handleEvent + * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel. + * @param event - the event to validate. Don't call getType() on it but use the `type` parameter instead. + * @param client - the client to get the current user and device id from + * @returns whether the event is valid and should be passed to handleEvent */ public static validateEvent(type: string, event: MatrixEvent, client: MatrixClient): boolean { const content = event.getContent(); @@ -234,7 +236,7 @@ export class VerificationRequest< /** * The key verification request event. - * @returns {MatrixEvent} The request event, or falsey if not found. + * @returns The request event, or falsey if not found. */ public get requestEvent(): MatrixEvent | undefined { return this.getEventByEither(REQUEST_TYPE); @@ -278,9 +280,9 @@ export class VerificationRequest< * This is useful when setting up the QR code UI, as it is somewhat asymmetrical: * if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa. * For methods that need to be supported by both ends, use the `methods` property. - * @param {string} method the method to check - * @param {boolean} force to check even if the phase is not ready or started yet, internal usage - * @return {boolean} whether or not the other party said the supported the method */ + * @param method - the method to check + * @param force - to check even if the phase is not ready or started yet, internal usage + * @returns whether or not the other party said the supported the method */ public otherPartySupportsMethod(method: string, force = false): boolean { if (!force && !this.ready && !this.started) { return false; @@ -398,7 +400,7 @@ export class VerificationRequest< * given the events sent so far in the verification. This is the * same algorithm used to determine which device to send the * verification to when no specific device is specified. - * @returns {{userId: *, deviceId: *}} The device information + * @returns The device information */ public get targetDevice(): ITargetDevice { const theirFirstEvent = @@ -415,10 +417,10 @@ export class VerificationRequest< /* Start the key verification, creating a verifier and sending a .start event. * If no previous events have been sent, pass in `targetDevice` to set who to direct this request to. - * @param {string} method the name of the verification method to use. - * @param {string?} targetDevice.userId the id of the user to direct this request to - * @param {string?} targetDevice.deviceId the id of the device to direct this request to - * @returns {VerifierBase} the verifier of the given method + * @param method - the name of the verification method to use. + * @param targetDevice.userId the id of the user to direct this request to + * @param targetDevice.deviceId the id of the device to direct this request to + * @returns the verifier of the given method */ public beginKeyVerification( method: VerificationMethod, @@ -448,7 +450,7 @@ export class VerificationRequest< /** * sends the initial .request event. - * @returns {Promise} resolves when the event has been sent. + * @returns resolves when the event has been sent. */ public async sendRequest(): Promise { if (!this.observeOnly && this._phase === PHASE_UNSENT) { @@ -459,9 +461,9 @@ export class VerificationRequest< /** * Cancels the request, sending a cancellation to the other party - * @param {string?} error.reason the error reason to send the cancellation with - * @param {string?} error.code the error code to send the cancellation with - * @returns {Promise} resolves when the event has been sent. + * @param reason - the error reason to send the cancellation with + * @param code - the error code to send the cancellation with + * @returns resolves when the event has been sent. */ public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise { if (!this.observeOnly && this._phase !== PHASE_CANCELLED) { @@ -478,7 +480,7 @@ export class VerificationRequest< /** * Accepts the request, sending a .ready event to the other party - * @returns {Promise} resolves when the event has been sent. + * @returns resolves when the event has been sent. */ public async accept(): Promise { if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) { @@ -491,10 +493,10 @@ export class VerificationRequest< /** * Can be used to listen for state changes until the callback returns true. - * @param {Function} fn callback to evaluate whether the request is in the desired state. + * @param fn - callback to evaluate whether the request is in the desired state. * Takes the request as an argument. - * @returns {Promise} that resolves once the callback returns true - * @throws {Error} when the request is cancelled + * @returns that resolves once the callback returns true + * @throws Error when the request is cancelled */ public waitFor(fn: (request: VerificationRequest) => boolean): Promise { return new Promise((resolve, reject) => { @@ -701,13 +703,13 @@ export class VerificationRequest< /** * Changes the state of the request and verifier in response to a key verification event. - * @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel. - * @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead. - * @param {boolean} isLiveEvent whether this is an even received through sync or not - * @param {boolean} isRemoteEcho whether this is the remote echo of an event sent by the same device - * @param {boolean} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers. + * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel. + * @param event - the event to handle. Don't call getType() on it but use the `type` parameter instead. + * @param isLiveEvent - whether this is an even received through sync or not + * @param isRemoteEcho - whether this is the remote echo of an event sent by the same device + * @param isSentByUs - whether this event is sent by a party that can accept and/or observe the request like one of our peers. * For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device. - * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. + * @returns a promise that resolves when any requests as an answer to the passed-in event are sent. */ public async handleEvent( type: string, diff --git a/src/embedded.ts b/src/embedded.ts index 27cf564a6..c5cbab80d 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -85,7 +85,7 @@ export interface ICapabilities { /** * Whether this client needs access to TURN servers. - * @default false + * @defaultValue false */ turnServers?: boolean; } diff --git a/src/event-mapper.ts b/src/event-mapper.ts index 6f2e25c1b..d40f579b5 100644 --- a/src/event-mapper.ts +++ b/src/event-mapper.ts @@ -20,8 +20,11 @@ import { IEvent, MatrixEvent, MatrixEventEvent } from "./models/event"; export type EventMapper = (obj: Partial) => MatrixEvent; export interface MapperOpts { + // don't re-emit events emitted on an event mapped by this mapper on the client preventReEmit?: boolean; + // decrypt event proactively decrypt?: boolean; + // the event is a to_device event toDevice?: boolean; } diff --git a/src/filter-component.ts b/src/filter-component.ts index 85afb0ea7..5a4601258 100644 --- a/src/filter-component.ts +++ b/src/filter-component.ts @@ -22,16 +22,12 @@ import { THREAD_RELATION_TYPE, } from "./models/thread"; -/** - * @module filter-component - */ - /** * Checks if a value matches a given field value, which may be a * terminated * wildcard pattern. - * @param {String} actualValue The value to be compared - * @param {String} filterValue The filter pattern to be compared - * @return {boolean} true if the actualValue matches the filterValue + * @param actualValue - The value to be compared + * @param filterValue - The filter pattern to be compared + * @returns true if the actualValue matches the filterValue */ function matchesWildcard(actualValue: string, filterValue: string): boolean { if (filterValue.endsWith("*")) { @@ -68,17 +64,14 @@ export interface IFilterComponent { * * N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as * 'Filters' are referred to as 'FilterCollections'. - * - * @constructor - * @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true } */ export class FilterComponent { public constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {} /** * Checks with the filter component matches the given event - * @param {MatrixEvent} event event to be checked against the filter - * @return {boolean} true if the event matches the filter + * @param event - event to be checked against the filter + * @returns true if the event matches the filter */ public check(event: MatrixEvent): boolean { const bundledRelationships = event.getUnsigned()?.["m.relations"] || {}; @@ -122,13 +115,13 @@ export class FilterComponent { /** * Checks whether the filter component matches the given event fields. - * @param {String} roomId the roomId for the event being checked - * @param {String} sender the sender of the event being checked - * @param {String} eventType the type of the event being checked - * @param {boolean} containsUrl whether the event contains a content.url field - * @param {boolean} relationTypes whether has aggregated relation of the given type - * @param {boolean} relationSenders whether one of the relation is sent by the user listed - * @return {boolean} true if the event fields match the filter + * @param roomId - the roomId for the event being checked + * @param sender - the sender of the event being checked + * @param eventType - the type of the event being checked + * @param containsUrl - whether the event contains a content.url field + * @param relationTypes - whether has aggregated relation of the given type + * @param relationSenders - whether one of the relation is sent by the user listed + * @returns true if the event fields match the filter */ private checkFields( roomId: string | undefined, @@ -148,17 +141,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; } @@ -194,8 +187,8 @@ export class FilterComponent { /** * Filters a list of events down to those which match this filter component - * @param {MatrixEvent[]} events Events to be checked against the filter component - * @return {MatrixEvent[]} events which matched the filter component + * @param events - Events to be checked against the filter component + * @returns events which matched the filter component */ public filter(events: MatrixEvent[]): MatrixEvent[] { return events.filter(this.check, this); @@ -204,7 +197,7 @@ export class FilterComponent { /** * Returns the limit field for a given filter component, providing a default of * 10 if none is otherwise specified. Cargo-culted from Synapse. - * @return {Number} the limit for this filter component. + * @returns the limit for this filter component. */ public limit(): number { return this.filterJson.limit !== undefined ? this.filterJson.limit : 10; diff --git a/src/filter.ts b/src/filter.ts index 57bd0540d..b5dc7140b 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module filter - */ - import { EventType, RelationType, @@ -27,12 +23,9 @@ import { FilterComponent, IFilterComponent } from "./filter-component"; import { MatrixEvent } from "./models/event"; /** - * @param {Object} obj - * @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 +63,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; @@ -79,14 +71,6 @@ interface IRoomFilter { } /* eslint-enable camelcase */ -/** - * Construct a new Filter. - * @constructor - * @param {string} userId The user ID for this filter. - * @param {string=} filterId The filter ID if known. - * @prop {string} userId The user ID of the filter - * @prop {?string} filterId The filter ID - */ export class Filter { public static LAZY_LOADING_MESSAGES_FILTER = { lazy_load_members: true, @@ -94,11 +78,6 @@ export class Filter { /** * Create a filter from existing data. - * @static - * @param {string} userId - * @param {string} filterId - * @param {Object} jsonObj - * @return {Filter} */ public static fromJson(userId: string | undefined | null, filterId: string, jsonObj: IFilterDefinition): Filter { const filter = new Filter(userId, filterId); @@ -110,11 +89,16 @@ export class Filter { private roomFilter?: FilterComponent; private roomTimelineFilter?: FilterComponent; + /** + * Construct a new Filter. + * @param userId - The user ID for this filter. + * @param filterId - The filter ID if known. + */ public constructor(public readonly userId: string | undefined | null, public filterId?: string) {} /** * Get the ID of this filter on your homeserver (if known) - * @return {?string} The filter ID + * @returns The filter ID */ public getFilterId(): string | undefined { return this.filterId; @@ -122,7 +106,7 @@ export class Filter { /** * Get the JSON body of the filter. - * @return {Object} The filter definition + * @returns The filter definition */ public getDefinition(): IFilterDefinition { return this.definition; @@ -130,7 +114,7 @@ export class Filter { /** * Set the JSON body of the filter - * @param {Object} definition The filter definition + * @param definition - The filter definition */ public setDefinition(definition: IFilterDefinition): void { this.definition = definition; @@ -199,7 +183,7 @@ export class Filter { /** * Get the room.timeline filter component of the filter - * @return {FilterComponent} room timeline filter component + * @returns room timeline filter component */ public getRoomTimelineFilterComponent(): FilterComponent | undefined { return this.roomTimelineFilter; @@ -208,8 +192,8 @@ export class Filter { /** * Filter the list of events based on whether they are allowed in a timeline * based on this filter - * @param {MatrixEvent[]} events the list of events being filtered - * @return {MatrixEvent[]} the list of events which match the filter + * @param events - the list of events being filtered + * @returns the list of events which match the filter */ public filterRoomTimeline(events: MatrixEvent[]): MatrixEvent[] { if (this.roomFilter) { @@ -223,7 +207,7 @@ export class Filter { /** * Set the max number of events to return for each room's timeline. - * @param {Number} limit The max number of events to return for each room. + * @param limit - The max number of events to return for each room. */ public setTimelineLimit(limit: number): void { setProp(this.definition, "room.timeline.limit", limit); @@ -231,7 +215,6 @@ export class Filter { /** * Enable threads unread notification - * @param {boolean} enabled */ public setUnreadThreadNotifications(enabled: boolean): void { this.definition = { @@ -252,7 +235,7 @@ export class Filter { /** * Control whether left rooms should be included in responses. - * @param {boolean} includeLeave True to make rooms the user has left appear + * @param includeLeave - True to make rooms the user has left appear * in responses. */ public setIncludeLeaveRooms(includeLeave: boolean): void { diff --git a/src/http-api/errors.ts b/src/http-api/errors.ts index 5ae0a0f50..e48fc029c 100644 --- a/src/http-api/errors.ts +++ b/src/http-api/errors.ts @@ -26,9 +26,8 @@ interface IErrorJson extends Partial { /** * Construct a generic HTTP error. This is a JavaScript Error with additional information * specific to HTTP responses. - * @constructor - * @param {string} msg The error message to include. - * @param {number} httpStatus The HTTP response status code. + * @param msg - The error message to include. + * @param httpStatus - The HTTP response status code. */ export class HTTPError extends Error { public constructor(msg: string, public readonly httpStatus?: number) { @@ -36,21 +35,18 @@ export class HTTPError extends Error { } } -/** - * Construct a Matrix error. This is a JavaScript Error with additional - * information specific to the standard Matrix error response. - * @constructor - * @param {Object} errorJson The Matrix error JSON returned from the homeserver. - * @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN". - * @prop {string} name Same as MatrixError.errcode but with a default unknown string. - * @prop {string} message The Matrix 'error' value, e.g. "Missing token." - * @prop {Object} data The raw Matrix error JSON used to construct this object. - * @prop {number} httpStatus The numeric HTTP status code given - */ export class MatrixError extends HTTPError { + // The Matrix 'errcode' value, e.g. "M_FORBIDDEN". public readonly errcode?: string; + // The raw Matrix error JSON used to construct this object. public data: IErrorJson; + /** + * Construct a Matrix error. This is a JavaScript Error with additional + * information specific to the standard Matrix error response. + * @param errorJson - The Matrix error JSON returned from the homeserver. + * @param httpStatus - The numeric HTTP status code given + */ public constructor( errorJson: IErrorJson = {}, public readonly httpStatus?: number, @@ -76,7 +72,6 @@ export class MatrixError extends HTTPError { * that a request failed because of some error with the connection, either * CORS was not correctly configured on the server, the server didn't response, * the request timed out, or the internet connection on the client side went down. - * @constructor */ export class ConnectionError extends Error { public constructor(message: string, cause?: Error) { diff --git a/src/http-api/fetch.ts b/src/http-api/fetch.ts index 71ba098e3..0c966d30d 100644 --- a/src/http-api/fetch.ts +++ b/src/http-api/fetch.ts @@ -16,7 +16,6 @@ limitations under the License. /** * This is an internal module. See {@link MatrixHttpApi} for the public class. - * @module http-api */ import * as utils from "../utils"; @@ -64,13 +63,13 @@ export class FetchHttpApi { /** * Sets the base URL for the identity server - * @param {string} url The new base url + * @param url - The new base url */ public setIdBaseUrl(url: string): void { this.opts.idBaseUrl = url; } - public idServerRequest( + public idServerRequest>( method: Method, path: string, params: Record | undefined, @@ -104,35 +103,29 @@ export class FetchHttpApi { /** * Perform an authorised request to the homeserver. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} path The HTTP path after the supplied prefix e.g. + * @param method - The HTTP method e.g. "GET". + * @param path - The HTTP path after the supplied prefix e.g. * "/createRoom". * - * @param {Object=} queryParams A dict of query params (these will NOT be + * @param queryParams - A dict of query params (these will NOT be * urlencoded). If unspecified, there will be no query params. * - * @param {Object} [body] The HTTP JSON body. + * @param body - The HTTP JSON body. * - * @param {Object|Number=} opts additional options. If a number is specified, + * @param opts - additional options. If a number is specified, * this is treated as `opts.localTimeoutMs`. * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. - * - * @param {string=} opts.prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. - * - * @param {string=} opts.baseUrl The alternative base url to use. - * If not specified, uses this.opts.baseUrl - * - * @param {Object=} opts.headers map of additional request headers - * - * @return {Promise} Resolves to {data: {Object}, - * headers: {Object}, code: {Number}}. - * If onlyData is set, this will resolve to the data - * object only. - * @return {module:http-api.MatrixError} Rejects with an error if a problem - * occurred. This includes network problems and Matrix-specific error JSON. + * @returns Promise which resolves to + * ``` + * { + * data: {Object}, + * headers: {Object}, + * code: {Number}, + * } + * ``` + * If `onlyData` is set, this will resolve to the `data` object only. + * @returns Rejects with an error if a problem occurred. + * This includes network problems and Matrix-specific error JSON. */ public authedRequest( method: Method, @@ -176,30 +169,28 @@ export class FetchHttpApi { /** * Perform a request to the homeserver without any credentials. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} path The HTTP path after the supplied prefix e.g. + * @param method - The HTTP method e.g. "GET". + * @param path - The HTTP path after the supplied prefix e.g. * "/createRoom". * - * @param {Object=} queryParams A dict of query params (these will NOT be + * @param queryParams - A dict of query params (these will NOT be * urlencoded). If unspecified, there will be no query params. * - * @param {Object} [body] The HTTP JSON body. + * @param body - The HTTP JSON body. * - * @param {Object=} opts additional options + * @param opts - additional options * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. - * - * @param {string=} opts.prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. - * - * @param {Object=} opts.headers map of additional request headers - * - * @return {Promise} Resolves to {data: {Object}, - * headers: {Object}, code: {Number}}. - * If onlyData is set, this will resolve to the data + * @returns Promise which resolves to + * ``` + * { + * data: {Object}, + * headers: {Object}, + * code: {Number}, + * } + * ``` + * If `onlyData is set, this will resolve to the data` * object only. - * @return {module:http-api.MatrixError} Rejects with an error if a problem + * @returns Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. */ public request( @@ -215,21 +206,16 @@ export class FetchHttpApi { /** * Perform a request to an arbitrary URL. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} url The HTTP URL object. + * @param method - The HTTP method e.g. "GET". + * @param url - The HTTP URL object. * - * @param {Object} [body] The HTTP JSON body. + * @param body - The HTTP JSON body. * - * @param {Object=} opts additional options + * @param opts - additional options * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. - * - * @param {Object=} opts.headers map of additional request headers - * - * @return {Promise} Resolves to data unless `onlyData` is specified as false, + * @returns Promise which resolves to data unless `onlyData` is specified as false, * where the resolved value will be a fetch Response object. - * @return {module:http-api.MatrixError} Rejects with an error if a problem + * @returns Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. */ public async requestOtherUrl( @@ -310,11 +296,11 @@ export class FetchHttpApi { /** * Form and return a homeserver request URL based on the given path params and prefix. - * @param {string} path The HTTP path after the supplied prefix e.g. "/createRoom". - * @param {Object} queryParams A dict of query params (these will NOT be urlencoded). - * @param {string} prefix The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix. - * @param {string} baseUrl The baseUrl to use e.g. "https://matrix.org/", defaulting to this.opts.baseUrl. - * @return {string} URL + * @param path - The HTTP path after the supplied prefix e.g. "/createRoom". + * @param queryParams - A dict of query params (these will NOT be urlencoded). + * @param prefix - The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix. + * @param baseUrl - The baseUrl to use e.g. "https://matrix.org/", defaulting to this.opts.baseUrl. + * @returns URL */ public getUrl( path: string, diff --git a/src/http-api/index.ts b/src/http-api/index.ts index c7f782d89..3574f539e 100644 --- a/src/http-api/index.ts +++ b/src/http-api/index.ts @@ -35,27 +35,13 @@ export class MatrixHttpApi extends FetchHttpApi { /** * Upload content to the homeserver * - * @param {object} file The object to upload. On a browser, something that + * @param file - The object to upload. On a browser, something that * can be sent to XMLHttpRequest.send (typically a File). Under node.js, * a Buffer, String or ReadStream. * - * @param {object} opts options object + * @param opts - options object * - * @param {string=} opts.name Name to give the file on the server. Defaults - * to file.name. - * - * @param {boolean=} opts.includeFilename if false will not send the filename, - * e.g for encrypted file uploads where filename leaks are undesirable. - * Defaults to true. - * - * @param {string=} opts.type Content-type for the upload. Defaults to - * file.type, or application/octet-stream. - * - * @param {Function=} opts.progressHandler Optional. Called when a chunk of - * data has been uploaded, with an object containing the fields `loaded` - * (number of bytes transferred) and `total` (total size, if known). - * - * @return {Promise} Resolves to response object, as + * @returns Promise which resolves to response object, as * determined by this.opts.onlyData, opts.rawResponse, and * opts.onlyContentUri. Rejects with an error (usually a MatrixError). */ @@ -190,7 +176,7 @@ export class MatrixHttpApi extends FetchHttpApi { /** * Get the content repository url with query parameters. - * @return {Object} An object with a 'base', 'path' and 'params' for base URL, + * @returns An object with a 'base', 'path' and 'params' for base URL, * path and query parameters respectively. */ public getContentUri(): IContentUri { diff --git a/src/http-api/interface.ts b/src/http-api/interface.ts index 372179470..9946aa37b 100644 --- a/src/http-api/interface.ts +++ b/src/http-api/interface.ts @@ -32,11 +32,25 @@ export interface IHttpOpts { } export interface IRequestOpts { + /** + * The alternative base url to use. + * If not specified, uses this.opts.baseUrl + */ baseUrl?: string; + /** + * The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + */ prefix?: string; - + /** + * map of additional request headers + */ headers?: Record; abortSignal?: AbortSignal; + /** + * The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + */ localTimeoutMs?: number; keepAlive?: boolean; // defaults to false json?: boolean; // defaults to true @@ -62,7 +76,29 @@ export enum HttpApiEvent { } export type HttpApiEventHandlerMap = { + /** + * Fires whenever the login session the JS SDK is using is no + * longer valid and the user must log in again. + * NB. This only fires when action is required from the user, not + * when then login session can be renewed by using a refresh token. + * @example + * ``` + * matrixClient.on("Session.logged_out", function(errorObj){ + * // show the login screen + * }); + * ``` + */ [HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void; + /** + * Fires when the JS SDK receives a M_CONSENT_NOT_GIVEN error in response + * to a HTTP request. + * @example + * ``` + * matrixClient.on("no_consent", function(message, contentUri) { + * console.info(message + ' Go to ' + contentUri); + * }); + * ``` + */ [HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void; }; @@ -72,9 +108,26 @@ export interface UploadProgress { } export interface UploadOpts { + /** + * Name to give the file on the server. Defaults to file.name. + */ name?: string; + /** + * Content-type for the upload. Defaults to + * file.type, or applicaton/octet-stream. + */ type?: string; + /** + * if false will not send the filename, + * e.g for encrypted file uploads where filename leaks are undesirable. + * Defaults to true. + */ includeFilename?: boolean; + /** + * Optional. Called when a chunk of + * data has been uploaded, with an object containing the fields `loaded` + * (number of bytes transferred) and `total` (total size, if known). + */ progressHandler?(progress: UploadProgress): void; abortController?: AbortController; } diff --git a/src/http-api/utils.ts b/src/http-api/utils.ts index 504660950..c49be740e 100644 --- a/src/http-api/utils.ts +++ b/src/http-api/utils.ts @@ -67,9 +67,9 @@ export function anySignal(signals: AbortSignal[]): { * If it is a JSON response, we will parse it into a MatrixError. Otherwise * we return a generic Error. * - * @param {XMLHttpRequest|Response} response response object - * @param {String} body raw body of the response - * @returns {Error} + * @param response - response object + * @param body - raw body of the response + * @returns */ export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error { let contentType: ParsedMediaType | null; @@ -102,8 +102,8 @@ function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest * * returns null if no content-type header could be found. * - * @param {XMLHttpRequest|Response} response response object - * @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found + * @param response - response object + * @returns parsed content-type header, or null if not found */ function getResponseContentType(response: XMLHttpRequest | Response): ParsedMediaType | null { let contentType: string | null; @@ -124,10 +124,10 @@ function getResponseContentType(response: XMLHttpRequest | Response): ParsedMedi /** * Retries a network operation run in a callback. - * @param {number} maxAttempts maximum attempts to try - * @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again. - * @return {any} the result of the network operation - * @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError + * @param maxAttempts - maximum attempts to try + * @param callback - callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again. + * @returns the result of the network operation + * @throws {@link ConnectionError} If after maxAttempts the callback still throws ConnectionError */ export async function retryNetworkOperation(maxAttempts: number, callback: () => Promise): Promise { let attempts = 0; diff --git a/src/indexeddb-helpers.ts b/src/indexeddb-helpers.ts index 695a3145f..6f99ae54b 100644 --- a/src/indexeddb-helpers.ts +++ b/src/indexeddb-helpers.ts @@ -18,9 +18,9 @@ limitations under the License. * Check if an IndexedDB database exists. The only way to do so is to try opening it, so * we do that and then delete it did not exist before. * - * @param {Object} indexedDB The `indexedDB` interface - * @param {string} dbName The database name to test for - * @returns {boolean} Whether the database exists + * @param indexedDB - The `indexedDB` interface + * @param dbName - The database name to test for + * @returns Whether the database exists */ export function exists(indexedDB: IDBFactory, dbName: string): Promise { return new Promise((resolve, reject) => { diff --git a/src/indexeddb-worker.ts b/src/indexeddb-worker.ts index 45facc485..78e87b373 100644 --- a/src/indexeddb-worker.ts +++ b/src/indexeddb-worker.ts @@ -20,6 +20,6 @@ limitations under the License. * to be used separately */ -/** The {@link module:indexeddb-store-worker~IndexedDBStoreWorker} class. */ +/** The {@link IndexedDBStoreWorker} class. */ export { IndexedDBStoreWorker } from "./store/indexeddb-store-worker"; diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts index df4a42c27..219569e61 100644 --- a/src/interactive-auth.ts +++ b/src/interactive-auth.ts @@ -16,8 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** @module interactive-auth */ - import { logger } from './logger'; import { MatrixClient } from "./client"; import { defer, IDeferred } from "./utils"; @@ -31,8 +29,11 @@ interface IFlow { } export interface IInputs { + // An email address. If supplied, a flow using email verification will be chosen. emailAddress?: string; + // An ISO two letter country code. Gives the country that opts.phoneNumber should be resolved relative to. phoneCountry?: string; + // A phone number. If supplied, a flow using phone number validation will be chosen. phoneNumber?: string; registrationToken?: string; } @@ -106,15 +107,66 @@ class NoAuthFlowFoundError extends Error { } interface IOpts { + /** + * A matrix client to use for the auth process + */ matrixClient: MatrixClient; + /** + * Error response from the last request. If null, a request will be made with no auth before starting. + */ authData?: IAuthData; + /** + * Inputs provided by the user and used by different stages of the auto process. + * The inputs provided will affect what flow is chosen. + */ inputs?: IInputs; + /** + * If resuming an existing interactive auth session, the sessionId of that session. + */ sessionId?: string; + /** + * If resuming an existing interactive auth session, the client secret for that session + */ clientSecret?: string; + /** + * If returning from having completed m.login.email.identity auth, the sid for the email verification session. + */ emailSid?: string; + + /** + * Called with the new auth dict to submit the request. + * Also passes a second deprecated arg which is a flag set to true if this request is a background request. + * The busyChanged callback should be used instead of the background flag. + * Should return a promise which resolves to the successful response or rejects with a MatrixError. + */ doRequest(auth: IAuthData | null, background: boolean): Promise; + /** + * Called when the status of the UI auth changes, + * ie. when the state of an auth stage changes of when the auth flow moves to a new stage. + * The arguments are: the login type (eg m.login.password); and an object which is either an error or an + * informational object specific to the login type. + * If the 'errcode' key is defined, the object is an error, and has keys: + * errcode: string, the textual error code, eg. M_UNKNOWN + * error: string, human readable string describing the error + * + * The login type specific objects are as follows: + * m.login.email.identity: + * * emailSid: string, the sid of the active email auth session + */ stateUpdated(nextStage: AuthType, status: IStageStatus): void; + + /** + * A function that takes the email address (string), clientSecret (string), attempt number (int) and + * sessionId (string) and calls the relevant requestToken function and returns the promise returned by that + * function. + * If the resulting promise rejects, the rejection will propagate through to the attemptAuth promise. + */ requestEmailToken(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>; + /** + * Called whenever the interactive auth logic becomes busy submitting information provided by the user or finishes. + * After this has been called with true the UI should indicate that a request is in progress + * until it is called again with false. + */ busyChanged?(busy: boolean): void; startAuthStage?(nextStage: string): Promise; // LEGACY } @@ -131,70 +183,7 @@ interface IOpts { * callbacks, and information gathered from the user can be submitted with * submitAuthDict. * - * @constructor - * @alias module:interactive-auth - * - * @param {object} opts options object - * - * @param {object} opts.matrixClient A matrix client to use for the auth process - * - * @param {object?} opts.authData error response from the last request. If - * null, a request will be made with no auth before starting. - * - * @param {function(object?): Promise} opts.doRequest - * called with the new auth dict to submit the request. Also passes a - * second deprecated arg which is a flag set to true if this request - * is a background request. The busyChanged callback should be used - * instead of the background flag. Should return a promise which resolves - * to the successful response or rejects with a MatrixError. - * - * @param {function(boolean): Promise} opts.busyChanged - * called whenever the interactive auth logic becomes busy submitting - * information provided by the user or finishes. After this has been - * called with true the UI should indicate that a request is in progress - * until it is called again with false. - * - * @param {function(string, object?)} opts.stateUpdated - * called when the status of the UI auth changes, ie. when the state of - * an auth stage changes of when the auth flow moves to a new stage. - * The arguments are: the login type (eg m.login.password); and an object - * which is either an error or an informational object specific to the - * login type. If the 'errcode' key is defined, the object is an error, - * and has keys: - * errcode: string, the textual error code, eg. M_UNKNOWN - * error: string, human readable string describing the error - * - * The login type specific objects are as follows: - * m.login.email.identity: - * * emailSid: string, the sid of the active email auth session - * - * @param {object?} opts.inputs Inputs provided by the user and used by different - * stages of the auto process. The inputs provided will affect what flow is chosen. - * - * @param {string?} opts.inputs.emailAddress An email address. If supplied, a flow - * using email verification will be chosen. - * - * @param {string?} opts.inputs.phoneCountry An ISO two letter country code. Gives - * the country that opts.phoneNumber should be resolved relative to. - * - * @param {string?} opts.inputs.phoneNumber A phone number. If supplied, a flow - * using phone number validation will be chosen. - * - * @param {string?} opts.sessionId If resuming an existing interactive auth session, - * the sessionId of that session. - * - * @param {string?} opts.clientSecret If resuming an existing interactive auth session, - * the client secret for that session - * - * @param {string?} opts.emailSid If returning from having completed m.login.email.identity - * auth, the sid for the email verification session. - * - * @param {function?} opts.requestEmailToken A function that takes the email address (string), - * clientSecret (string), attempt number (int) and sessionId (string) and calls the - * relevant requestToken function and returns the promise returned by that function. - * If the resulting promise rejects, the rejection will propagate through to the - * attemptAuth promise. - * + * @param opts - options object */ export class InteractiveAuth { private readonly matrixClient: MatrixClient; @@ -236,7 +225,7 @@ export class InteractiveAuth { /** * begin the authentication process. * - * @return {Promise} which resolves to the response on success, + * @returns which resolves to the response on success, * or rejects with the error on failure. Rejects with NoAuthFlowFoundError if * no suitable authentication flow can be found */ @@ -307,7 +296,7 @@ export class InteractiveAuth { /** * get the auth session ID * - * @return {string} session id + * @returns session id */ public getSessionId(): string | undefined { return this.data?.session; @@ -317,7 +306,7 @@ export class InteractiveAuth { * get the client secret used for validation sessions * with the identity server. * - * @return {string} client secret + * @returns client secret */ public getClientSecret(): string { return this.clientSecret; @@ -326,8 +315,8 @@ export class InteractiveAuth { /** * get the server params for a given stage * - * @param {string} loginType login type for the stage - * @return {object?} any parameters from the server for this stage + * @param loginType - login type for the stage + * @returns any parameters from the server for this stage */ public getStageParams(loginType: string): Record | undefined { return this.data.params?.[loginType]; @@ -342,10 +331,10 @@ export class InteractiveAuth { * make attemptAuth resolve/reject, or cause the startAuthStage callback * to be called for a new stage. * - * @param {object} authData new auth dict to send to the server. Should + * @param authData - new auth dict to send to the server. Should * include a `type` property denoting the login type, as well as any * other params for that stage. - * @param {boolean} background If true, this request failing will not result + * @param background - If true, this request failing will not result * in the attemptAuth promise being rejected. This can be set to true * for requests that just poll to see if auth has been completed elsewhere. */ @@ -398,7 +387,7 @@ export class InteractiveAuth { * Gets the sid for the email validation session * Specific to m.login.email.identity * - * @returns {string} The sid of the email auth session + * @returns The sid of the email auth session */ public getEmailSid(): string | undefined { return this.emailSid; @@ -410,7 +399,7 @@ export class InteractiveAuth { * of the email validation. * Specific to m.login.email.identity * - * @param {string} sid The sid for the email validation session + * @param sid - The sid for the email validation session */ public setEmailSid(sid: string): void { this.emailSid = sid; @@ -448,9 +437,9 @@ export class InteractiveAuth { * Fire off a request, and either resolve the promise, or call * startAuthStage. * - * @private - * @param {object?} auth new auth dict, including session id - * @param {boolean?} background If true, this request is a background poll, so it + * @internal + * @param auth - new auth dict, including session id + * @param background - If true, this request is a background poll, so it * failing will not result in the attemptAuth promise being rejected. * This can be set to true for requests that just poll to see if auth has * been completed elsewhere. @@ -526,8 +515,8 @@ export class InteractiveAuth { /** * Pick the next stage and call the callback * - * @private - * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + * @internal + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found */ private startNextAuthStage(): void { const nextStage = this.chooseStage(); @@ -559,9 +548,9 @@ export class InteractiveAuth { /** * Pick the next auth stage * - * @private - * @return {string?} login type - * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + * @internal + * @returns login type + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found */ private chooseStage(): AuthType | undefined { if (this.chosenFlow === null) { @@ -584,9 +573,9 @@ export class InteractiveAuth { * this could result in the email not being used which would leave * the account with no means to reset a password. * - * @private - * @return {object} flow - * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + * @internal + * @returns flow + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found */ private chooseFlow(): IFlow { const flows = this.data.flows || []; @@ -625,9 +614,8 @@ export class InteractiveAuth { /** * Get the first uncompleted stage in the given flow * - * @private - * @param {object} flow - * @return {string} login type + * @internal + * @returns login type */ private firstUncompletedStage(flow: IFlow): AuthType | undefined { const completed = this.data.completed || []; diff --git a/src/logger.ts b/src/logger.ts index de1ca6619..7077dee02 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -15,10 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module logger - */ - import log, { Logger } from "loglevel"; // This is to demonstrate, that you can use any namespace you want. @@ -56,7 +52,7 @@ log.methodFactory = function(methodName, logLevel, loggerName) { }; /** - * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. + * Drop-in replacement for `console` using {@link https://www.npmjs.com/package/loglevel|loglevel}. * Can be tailored down to specific use cases if needed. */ export const logger = log.getLogger(DEFAULT_NAMESPACE) as PrefixedLogger; diff --git a/src/matrix.ts b/src/matrix.ts index 421e0e6ed..d075c098e 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -70,8 +70,7 @@ let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore; /** * Configure a different factory to be used for creating crypto stores * - * @param {Function} fac a function which will return a new - * {@link module:crypto.store.base~CryptoStore}. + * @param fac - a function which will return a new {@link CryptoStore} */ export function setCryptoStoreFactory(fac: () => CryptoStore): void { cryptoStoreFactory = fac; @@ -88,24 +87,14 @@ function amendClientOpts(opts: ICreateClientOpts): ICreateClientOpts { } /** - * Construct a Matrix Client. Similar to {@link module:client.MatrixClient} + * Construct a Matrix Client. Similar to {@link MatrixClient} * except that the 'request', 'store' and 'scheduler' dependencies are satisfied. - * @param {Object} opts The configuration options for this client. These configuration - * options will be passed directly to {@link module:client.MatrixClient}. - * @param {Object} opts.store If not set, defaults to - * {@link module:store/memory.MemoryStore}. - * @param {Object} opts.scheduler If not set, defaults to - * {@link module:scheduler~MatrixScheduler}. + * @param opts - The configuration options for this client. These configuration + * options will be passed directly to {@link MatrixClient}. * - * @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore - * crypto store implementation. Calls the factory supplied to - * {@link setCryptoStoreFactory} if unspecified; or if no factory has been - * specified, uses a default implementation (indexeddb in the browser, - * in-memory otherwise). - * - * @return {MatrixClient} A new matrix client. - * @see {@link module:client.MatrixClient} for the full list of options for - * opts. + * @returns A new matrix client. + * @see {@link MatrixClient} for the full list of options for + * `opts`. */ export function createClient(opts: ICreateClientOpts): MatrixClient { return new MatrixClient(amendClientOpts(opts)); diff --git a/src/models/MSC3089Branch.ts b/src/models/MSC3089Branch.ts index 25ce51a20..e4c0eb287 100644 --- a/src/models/MSC3089Branch.ts +++ b/src/models/MSC3089Branch.ts @@ -67,7 +67,7 @@ export class MSC3089Branch { /** * Deletes the file from the tree, including all prior edits/versions. - * @returns {Promise} Resolves when complete. + * @returns Promise which resolves when complete. */ public async delete(): Promise { await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {}, this.id); @@ -79,7 +79,7 @@ export class MSC3089Branch { /** * Gets the name for this file. - * @returns {string} The name, or "Unnamed File" if unknown. + * @returns The name, or "Unnamed File" if unknown. */ public getName(): string { return this.indexEvent.getContent()['name'] || "Unnamed File"; @@ -87,8 +87,8 @@ export class MSC3089Branch { /** * Sets the name for this file. - * @param {string} name The new name for this file. - * @returns {Promise} Resolves when complete. + * @param name - The new name for this file. + * @returns Promise which resolves when complete. */ public async setName(name: string): Promise { await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, { @@ -99,7 +99,7 @@ export class MSC3089Branch { /** * Gets whether or not a file is locked. - * @returns {boolean} True if locked, false otherwise. + * @returns True if locked, false otherwise. */ public isLocked(): boolean { return this.indexEvent.getContent()['locked'] || false; @@ -107,8 +107,8 @@ export class MSC3089Branch { /** * Sets a file as locked or unlocked. - * @param {boolean} locked True to lock the file, false otherwise. - * @returns {Promise} Resolves when complete. + * @param locked - True to lock the file, false otherwise. + * @returns Promise which resolves when complete. */ public async setLocked(locked: boolean): Promise { await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, { @@ -119,7 +119,7 @@ export class MSC3089Branch { /** * Gets information about the file needed to download it. - * @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file. + * @returns Information about the file. */ public async getFileInfo(): Promise<{ info: IEncryptedFile, httpUrl: string }> { const event = await this.getFileEvent(); @@ -136,7 +136,7 @@ export class MSC3089Branch { /** * Gets the event the file points to. - * @returns {Promise} Resolves to the file's event. + * @returns Promise which resolves to the file's event. */ public async getFileEvent(): Promise { const room = this.client.getRoom(this.roomId); @@ -161,11 +161,11 @@ export class MSC3089Branch { /** * Creates a new version of this file with contents in a type that is compatible with MatrixClient.uploadContent(). - * @param {string} name The name of the file. - * @param {File | String | Buffer | ReadStream | Blob} encryptedContents The encrypted contents. - * @param {Partial} info The encrypted file information. - * @param {IContent} additionalContent Optional event content fields to include in the message. - * @returns {Promise} Resolves to the file event's sent response. + * @param name - The name of the file. + * @param encryptedContents - The encrypted contents. + * @param info - The encrypted file information. + * @param additionalContent - Optional event content fields to include in the message. + * @returns Promise which resolves to the file event's sent response. */ public async createNewVersion( name: string, @@ -200,7 +200,7 @@ export class MSC3089Branch { /** * Gets the file's version history, starting at this file. - * @returns {Promise} Resolves to the file's version history, with the + * @returns Promise which resolves to the file's version history, with the * first element being the current version and the last element being the first version. */ public async getVersionHistory(): Promise { diff --git a/src/models/MSC3089TreeSpace.ts b/src/models/MSC3089TreeSpace.ts index f437eab84..76c733996 100644 --- a/src/models/MSC3089TreeSpace.ts +++ b/src/models/MSC3089TreeSpace.ts @@ -111,8 +111,8 @@ export class MSC3089TreeSpace { /** * Sets the name of the tree space. - * @param {string} name The new name for the space. - * @returns {Promise} Resolves when complete. + * @param name - The new name for the space. + * @returns Promise which resolves when complete. */ public async setName(name: string): Promise { await this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, ""); @@ -121,15 +121,15 @@ export class MSC3089TreeSpace { /** * Invites a user to the tree space. They will be given the default Viewer * permission level unless specified elsewhere. - * @param {string} userId The user ID to invite. - * @param {boolean} andSubspaces True (default) to invite the user to all + * @param userId - The user ID to invite. + * @param andSubspaces - True (default) to invite the user to all * directories/subspaces too, recursively. - * @param {boolean} shareHistoryKeys True (default) to share encryption keys + * @param shareHistoryKeys - True (default) to share encryption keys * with the invited user. This will allow them to decrypt the events (files) * in the tree. Keys will not be shared if the room is lacking appropriate * history visibility (by default, history visibility is "shared" in trees, * which is an appropriate visibility for these purposes). - * @returns {Promise} Resolves when complete. + * @returns Promise which resolves when complete. */ public async invite(userId: string, andSubspaces = true, shareHistoryKeys = true): Promise { const promises: Promise[] = [this.retryInvite(userId)]; @@ -164,9 +164,9 @@ export class MSC3089TreeSpace { * Sets the permissions of a user to the given role. Note that if setting a user * to Owner then they will NOT be able to be demoted. If the user does not have * permission to change the power level of the target, an error will be thrown. - * @param {string} userId The user ID to change the role of. - * @param {TreePermissions} role The role to assign. - * @returns {Promise} Resolves when complete. + * @param userId - The user ID to change the role of. + * @param role - The role to assign. + * @returns Promise which resolves when complete. */ public async setPermissions(userId: string, role: TreePermissions): Promise { const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); @@ -200,8 +200,8 @@ export class MSC3089TreeSpace { * Gets the current permissions of a user. Note that any users missing explicit permissions (or not * in the space) will be considered Viewers. Appropriate membership checks need to be performed * elsewhere. - * @param {string} userId The user ID to check permissions of. - * @returns {TreePermissions} The permissions for the user, defaulting to Viewer. + * @param userId - The user ID to check permissions of. + * @returns The permissions for the user, defaulting to Viewer. */ public getPermissions(userId: string): TreePermissions { const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); @@ -220,8 +220,8 @@ export class MSC3089TreeSpace { /** * Creates a directory under this tree space, represented as another tree space. - * @param {string} name The name for the directory. - * @returns {Promise} Resolves to the created directory. + * @param name - The name for the directory. + * @returns Promise which resolves to the created directory. */ public async createDirectory(name: string): Promise { const directory = await this.client.unstableCreateFileTree(name); @@ -239,7 +239,7 @@ export class MSC3089TreeSpace { /** * Gets a list of all known immediate subdirectories to this tree space. - * @returns {MSC3089TreeSpace[]} The tree spaces (directories). May be empty, but not null. + * @returns The tree spaces (directories). May be empty, but not null. */ public getDirectories(): MSC3089TreeSpace[] { const trees: MSC3089TreeSpace[] = []; @@ -261,8 +261,8 @@ export class MSC3089TreeSpace { /** * Gets a subdirectory of a given ID under this tree space. Note that this will not recurse * into children and instead only look one level deep. - * @param {string} roomId The room ID (directory ID) to find. - * @returns {MSC3089TreeSpace | undefined} The directory, or undefined if not found. + * @param roomId - The room ID (directory ID) to find. + * @returns The directory, or undefined if not found. */ public getDirectory(roomId: string): MSC3089TreeSpace | undefined { return this.getDirectories().find(r => r.roomId === roomId); @@ -270,7 +270,7 @@ export class MSC3089TreeSpace { /** * Deletes the tree, kicking all members and deleting **all subdirectories**. - * @returns {Promise} Resolves when complete. + * @returns Promise which resolves when complete. */ public async delete(): Promise { const subdirectories = this.getDirectories(); @@ -341,7 +341,7 @@ export class MSC3089TreeSpace { /** * Gets the current order index for this directory. Note that if this is the top level space * then -1 will be returned. - * @returns {number} The order index of this space. + * @returns The order index of this space. */ public getOrder(): number { if (this.isTopLevel) return -1; @@ -357,8 +357,8 @@ export class MSC3089TreeSpace { * Sets the order index for this directory within its parent. Note that if this is a top level * space then an error will be thrown. -1 can be used to move the child to the start, and numbers * larger than the number of children can be used to move the child to the end. - * @param {number} index The new order index for this space. - * @returns {Promise} Resolves when complete. + * @param index - The new order index for this space. + * @returns Promise which resolves when complete. * @throws Throws if this is a top level space. */ public async setOrder(index: number): Promise { @@ -464,11 +464,11 @@ export class MSC3089TreeSpace { /** * Creates (uploads) a new file to this tree. The file must have already been encrypted for the room. * The file contents are in a type that is compatible with MatrixClient.uploadContent(). - * @param {string} name The name of the file. - * @param {File | String | Buffer | ReadStream | Blob} encryptedContents The encrypted contents. - * @param {Partial} info The encrypted file information. - * @param {IContent} additionalContent Optional event content fields to include in the message. - * @returns {Promise} Resolves to the file event's sent response. + * @param name - The name of the file. + * @param encryptedContents - The encrypted contents. + * @param info - The encrypted file information. + * @param additionalContent - Optional event content fields to include in the message. + * @returns Promise which resolves to the file event's sent response. */ public async createFile( name: string, @@ -512,8 +512,8 @@ export class MSC3089TreeSpace { /** * Retrieves a file from the tree. - * @param {string} fileEventId The event ID of the file. - * @returns {MSC3089Branch | null} The file, or null if not found. + * @param fileEventId - The event ID of the file. + * @returns The file, or null if not found. */ public getFile(fileEventId: string): MSC3089Branch | null { const branch = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name, fileEventId); @@ -522,7 +522,7 @@ export class MSC3089TreeSpace { /** * Gets an array of all known files for the tree. - * @returns {MSC3089Branch[]} The known files. May be empty, but not null. + * @returns The known files. May be empty, but not null. */ public listFiles(): MSC3089Branch[] { return this.listAllFiles().filter(b => b.isActive); @@ -530,7 +530,7 @@ export class MSC3089TreeSpace { /** * Gets an array of all known files for the tree, including inactive/invalid ones. - * @returns {MSC3089Branch[]} The known files. May be empty, but not null. + * @returns The known files. May be empty, but not null. */ public listAllFiles(): MSC3089Branch[] { const branches = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name) ?? []; diff --git a/src/models/event-context.ts b/src/models/event-context.ts index 60252627b..0401cd530 100644 --- a/src/models/event-context.ts +++ b/src/models/event-context.ts @@ -17,9 +17,6 @@ limitations under the License. import { MatrixEvent } from "./event"; import { Direction } from "./event-timeline"; -/** - * @module models/event-context - */ export class EventContext { private timeline: MatrixEvent[]; private ourEventIndex = 0; @@ -38,9 +35,7 @@ export class EventContext { * It also stores pagination tokens for going backwards and forwards in the * timeline. * - * @param {MatrixEvent} ourEvent the event at the centre of this context - * - * @constructor + * @param ourEvent - the event at the centre of this context */ public constructor(public readonly ourEvent: MatrixEvent) { this.timeline = [ourEvent]; @@ -51,7 +46,7 @@ export class EventContext { * * This is a convenience function for getTimeline()[getOurEventIndex()]. * - * @return {MatrixEvent} The event at the centre of this context. + * @returns The event at the centre of this context. */ public getEvent(): MatrixEvent { return this.timeline[this.ourEventIndex]; @@ -60,7 +55,7 @@ export class EventContext { /** * Get the list of events in this context * - * @return {Array} An array of MatrixEvents + * @returns An array of MatrixEvents */ public getTimeline(): MatrixEvent[] { return this.timeline; @@ -68,8 +63,6 @@ export class EventContext { /** * Get the index in the timeline of our event - * - * @return {Number} */ public getOurEventIndex(): number { return this.ourEventIndex; @@ -78,9 +71,7 @@ export class EventContext { /** * Get a pagination token. * - * @param {boolean} backwards true to get the pagination token for going - * backwards in time - * @return {string} + * @param backwards - true to get the pagination token for going */ public getPaginateToken(backwards = false): string | null { return this.paginateTokens[backwards ? Direction.Backward : Direction.Forward]; @@ -91,8 +82,8 @@ export class EventContext { * * Generally this will be used only by the matrix js sdk. * - * @param {string} token pagination token - * @param {boolean} backwards true to set the pagination token for going + * @param token - pagination token + * @param backwards - true to set the pagination token for going * backwards in time */ public setPaginateToken(token?: string, backwards = false): void { @@ -102,8 +93,8 @@ export class EventContext { /** * Add more events to the timeline * - * @param {Array} events new events, in timeline order - * @param {boolean} atStart true to insert new events at the start + * @param events - new events, in timeline order + * @param atStart - true to insert new events at the start */ public addEvents(events: MatrixEvent[], atStart = false): void { // TODO: should we share logic with Room.addEventsToTimeline? diff --git a/src/models/event-status.ts b/src/models/event-status.ts index faca97186..a5113e0b7 100644 --- a/src/models/event-status.ts +++ b/src/models/event-status.ts @@ -17,7 +17,6 @@ limitations under the License. /** * Enum for event statuses. * @readonly - * @enum {string} */ export enum EventStatus { /** The event was not sent and will no longer be retried. */ diff --git a/src/models/event-timeline-set.ts b/src/models/event-timeline-set.ts index 6dd2a0e77..e1da746c9 100644 --- a/src/models/event-timeline-set.ts +++ b/src/models/event-timeline-set.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module models/event-timeline-set - */ - import { EventTimeline, IAddEventOptions } from "./event-timeline"; import { MatrixEvent } from "./event"; import { logger } from '../logger'; @@ -31,16 +27,20 @@ import { Thread, ThreadFilterType } from "./thread"; const DEBUG = true; +/* istanbul ignore next */ let debuglog: (...args: any[]) => void; if (DEBUG) { // using bind means that we get to keep useful line numbers in the console debuglog = logger.log.bind(logger); } else { + /* istanbul ignore next */ debuglog = function(): void {}; } interface IOpts { + // Set to true to enable improved timeline support. timelineSupport?: boolean; + // The filter object, if any, for this timelineSet. filter?: Filter; pendingEvents?: boolean; } @@ -51,7 +51,9 @@ export enum DuplicateStrategy { } export interface IRoomTimelineData { + // the timeline the event was added to/removed from timeline: EventTimeline; + // true if the event was a real-time event added to the end of the live timeline liveEvent?: boolean; } @@ -75,6 +77,26 @@ export interface IAddLiveEventOptions type EmittedEvents = RoomEvent.Timeline | RoomEvent.TimelineReset; export type EventTimelineSetHandlerMap = { + /** + * Fires whenever the timeline in a room is updated. + * @param event - The matrix event which caused this event to fire. + * @param room - The room, if any, whose timeline was updated. + * @param toStartOfTimeline - True if this event was added to the start + * @param removed - True if this event has just been removed from the timeline + * (beginning; oldest) of the timeline e.g. due to pagination. + * + * @param data - more data about the event + * + * @example + * ``` + * matrixClient.on("Room.timeline", + * function(event, room, toStartOfTimeline, removed, data) { + * if (!toStartOfTimeline && data.liveEvent) { + * var messageToAppend = room.timeline.[room.timeline.length - 1]; + * } + * }); + * ``` + */ [RoomEvent.Timeline]: ( event: MatrixEvent, room: Room | undefined, @@ -82,6 +104,18 @@ export type EventTimelineSetHandlerMap = { removed: boolean, data: IRoomTimelineData, ) => void; + /** + * Fires whenever the live timeline in a room is reset. + * + * When we get a 'limited' sync (for example, after a network outage), we reset + * the live timeline to be empty before adding the recent events to the new + * timeline. This event is fired after the timeline is reset, and before the + * new events are added. + * + * @param room - The room whose live timeline was reset, if any + * @param timelineSet - timelineSet room whose live timeline was reset + * @param resetAllTimelines - True if all timelines were reset. + */ [RoomEvent.TimelineReset]: ( room: Room | undefined, eventTimelineSet: EventTimelineSet, @@ -119,20 +153,13 @@ export class EventTimelineSet extends TypedEventEmitterIn order that we can find events from their ids later, we also maintain a * map from event_id to timeline and index. * - * @constructor - * @param {Room=} room - * Room for this timelineSet. May be null for non-room cases, such as the + * @param room - Room for this timelineSet. May be null for non-room cases, such as the * notification timeline. - * @param {Object} opts Options inherited from Room. - * - * @param {boolean} [opts.timelineSupport = false] - * Set to true to enable improved timeline support. - * @param {Object} [opts.filter = null] - * The filter object, if any, for this timelineSet. - * @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet, + * @param opts - Options inherited from Room. + * @param client - the Matrix client which owns this EventTimelineSet, * can be omitted if room is specified. - * @param {Thread=} thread the thread to which this timeline set relates. - * @param {boolean} isThreadTimeline Whether this timeline set relates to a thread list timeline + * @param thread - the thread to which this timeline set relates. + * @param isThreadTimeline - Whether this timeline set relates to a thread list timeline * (e.g., All threads or My threads) */ public constructor( @@ -159,7 +186,7 @@ export class EventTimelineSet extends TypedEventEmitteropts.pendingEventOrdering was not 'detached' + * @throws If `opts.pendingEventOrdering` was not 'detached' */ public getPendingEvents(): MatrixEvent[] { if (!this.room || !this.displayPendingEvents) { @@ -201,7 +228,7 @@ export class EventTimelineSet extends TypedEventEmitterThis is used when /sync returns a 'limited' timeline. * - * @param {string=} backPaginationToken token for back-paginating the new timeline - * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline, + * @param backPaginationToken - token for back-paginating the new timeline + * @param forwardPaginationToken - token for forward-paginating the old live timeline, * if absent or null, all timelines are reset. * - * @fires module:client~MatrixClient#event:"Room.timelineReset" + * @remarks + * Fires {@link RoomEvent.TimelineReset} */ public resetLiveTimeline(backPaginationToken?: string, forwardPaginationToken?: string): void { // Each EventTimeline has RoomState objects tracking the state at the start @@ -293,8 +321,8 @@ export class EventTimelineSet extends TypedEventEmitterWill fire "Room.timeline" for each event added. * - * @param {MatrixEvent[]} events A list of events to add. + * @param events - A list of events to add. * - * @param {boolean} toStartOfTimeline True to add these events to the start + * @param toStartOfTimeline - True to add these events to the start * (oldest) instead of the end (newest) of the timeline. If true, the oldest * event will be the last element of 'events'. * - * @param {module:models/event-timeline~EventTimeline} timeline timeline to + * @param timeline - timeline to * add events to. * - * @param {string=} paginationToken token for the next batch of events + * @param paginationToken - token for the next batch of events * - * @fires module:client~MatrixClient#event:"Room.timeline" + * @remarks + * Fires {@link RoomEvent.Timeline} * */ public addEventsToTimeline( @@ -557,8 +586,8 @@ export class EventTimelineSet extends TypedEventEmitterOnce a timeline joins up with its neighbour, they are linked together into a * doubly-linked list. * - * @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of - * @constructor + * @param eventTimelineSet - the set of timelines this is part of */ public constructor(private readonly eventTimelineSet: EventTimelineSet) { this.roomId = eventTimelineSet.room?.roomId ?? null; @@ -148,9 +143,9 @@ export class EventTimeline { * *

This can only be called before any events are added. * - * @param {MatrixEvent[]} stateEvents list of state events to initialise the + * @param stateEvents - list of state events to initialise the * state with. - * @throws {Error} if an attempt is made to call this after addEvent is called. + * @throws Error if an attempt is made to call this after addEvent is called. */ public initialiseState(stateEvents: MatrixEvent[], { timelineWasEmpty }: IInitialiseStateOptions = {}): void { if (this.events.length > 0) { @@ -167,11 +162,11 @@ export class EventTimeline { * The end state of this timeline gets replaced with an independent copy of the current RoomState, * and will need a new pagination token if it ever needs to paginate forwards. - * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * @param direction - EventTimeline.BACKWARDS to get the state at the * start of the timeline; EventTimeline.FORWARDS to get the state at the end * of the timeline. * - * @return {EventTimeline} the new timeline + * @returns the new timeline */ public forkLive(direction: Direction): EventTimeline { const forkState = this.getState(direction); @@ -191,11 +186,11 @@ export class EventTimeline { /** * Creates an independent timeline, inheriting the directional state from this timeline. * - * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * @param direction - EventTimeline.BACKWARDS to get the state at the * start of the timeline; EventTimeline.FORWARDS to get the state at the end * of the timeline. * - * @return {EventTimeline} the new timeline + * @returns the new timeline */ public fork(direction: Direction): EventTimeline { const forkState = this.getState(direction); @@ -207,7 +202,7 @@ export class EventTimeline { /** * Get the ID of the room for this timeline - * @return {string} room ID + * @returns room ID */ public getRoomId(): string | null { return this.roomId; @@ -215,7 +210,7 @@ export class EventTimeline { /** * Get the filter for this timeline's timelineSet (if any) - * @return {Filter} filter + * @returns filter */ public getFilter(): Filter | undefined { return this.eventTimelineSet.getFilter(); @@ -223,7 +218,7 @@ export class EventTimeline { /** * Get the timelineSet for this timeline - * @return {EventTimelineSet} timelineSet + * @returns timelineSet */ public getTimelineSet(): EventTimelineSet { return this.eventTimelineSet; @@ -237,8 +232,6 @@ export class EventTimeline { * relative to the base index (although note that a given event's index may * well be less than the base index, thus giving that event a negative relative * index). - * - * @return {number} */ public getBaseIndex(): number { return this.baseIndex; @@ -247,7 +240,7 @@ export class EventTimeline { /** * Get the list of events in this context * - * @return {MatrixEvent[]} An array of MatrixEvents + * @returns An array of MatrixEvents */ public getEvents(): MatrixEvent[] { return this.events; @@ -256,11 +249,11 @@ export class EventTimeline { /** * Get the room state at the start/end of the timeline * - * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * @param direction - EventTimeline.BACKWARDS to get the state at the * start of the timeline; EventTimeline.FORWARDS to get the state at the end * of the timeline. * - * @return {RoomState} state at the start/end of the timeline + * @returns state at the start/end of the timeline */ public getState(direction: Direction): RoomState | undefined { if (direction == EventTimeline.BACKWARDS) { @@ -275,11 +268,11 @@ export class EventTimeline { /** * Get a pagination token * - * @param {string} direction EventTimeline.BACKWARDS to get the pagination + * @param direction - EventTimeline.BACKWARDS to get the pagination * token for going backwards in time; EventTimeline.FORWARDS to get the * pagination token for going forwards in time. * - * @return {?string} pagination token + * @returns pagination token */ public getPaginationToken(direction: Direction): string | null { if (this.roomId) { @@ -294,9 +287,9 @@ export class EventTimeline { /** * Set a pagination token * - * @param {?string} token pagination token + * @param token - pagination token * - * @param {string} direction EventTimeline.BACKWARDS to set the pagination + * @param direction - EventTimeline.BACKWARDS to set the pagination * token for going backwards in time; EventTimeline.FORWARDS to set the * pagination token for going forwards in time. */ @@ -313,10 +306,10 @@ export class EventTimeline { /** * Get the next timeline in the series * - * @param {string} direction EventTimeline.BACKWARDS to get the previous + * @param direction - EventTimeline.BACKWARDS to get the previous * timeline; EventTimeline.FORWARDS to get the next timeline. * - * @return {?EventTimeline} previous or following timeline, if they have been + * @returns previous or following timeline, if they have been * joined up. */ public getNeighbouringTimeline(direction: Direction): EventTimeline | null { @@ -332,12 +325,12 @@ export class EventTimeline { /** * Set the next timeline in the series * - * @param {EventTimeline} neighbour previous/following timeline + * @param neighbour - previous/following timeline * - * @param {string} direction EventTimeline.BACKWARDS to set the previous + * @param direction - EventTimeline.BACKWARDS to set the previous * timeline; EventTimeline.FORWARDS to set the next timeline. * - * @throws {Error} if an attempt is made to set the neighbouring timeline when + * @throws Error if an attempt is made to set the neighbouring timeline when * it is already set. */ public setNeighbouringTimeline(neighbour: EventTimeline, direction: Direction): void { @@ -361,8 +354,8 @@ export class EventTimeline { /** * Add a new event to the timeline, and update the state * - * @param {MatrixEvent} event new event - * @param {IAddEventOptions} options addEvent options + * @param event - new event + * @param options - addEvent options */ public addEvent( event: MatrixEvent, @@ -447,8 +440,8 @@ export class EventTimeline { /** * Remove an event from the timeline * - * @param {string} eventId ID of event to be removed - * @return {?MatrixEvent} removed event, or null if not found + * @param eventId - ID of event to be removed + * @returns removed event, or null if not found */ public removeEvent(eventId: string): MatrixEvent | null { for (let i = this.events.length - 1; i >= 0; i--) { @@ -467,7 +460,7 @@ export class EventTimeline { /** * Return a string to identify this timeline, for debugging * - * @return {string} name for this timeline + * @returns name for this timeline */ public toString(): string { return this.name; diff --git a/src/models/event.ts b/src/models/event.ts index c233307d4..2b082f9b6 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -17,7 +17,6 @@ limitations under the License. /** * This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for * the public classes. - * @module models/event */ import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk"; @@ -80,15 +79,15 @@ export interface IEvent { redacts?: string; /** - * @deprecated + * @deprecated in favour of `sender` */ user_id?: string; /** - * @deprecated + * @deprecated in favour of `unsigned.prev_content` */ prev_content?: IContent; /** - * @deprecated + * @deprecated in favour of `origin_server_ts` */ age?: number; } @@ -150,8 +149,11 @@ interface IKeyRequestRecipient { } export interface IDecryptOptions { + // Emits "event.decrypted" if set to true emit?: boolean; + // True if this is a retry (enables more logging) isRetry?: boolean; + // whether the message should be re-decrypted if it was previously successfully decrypted with an untrusted key forceRedecryptIfUntrusted?: boolean; } @@ -192,6 +194,12 @@ export enum MatrixEventEvent { export type MatrixEventEmittedEvents = MatrixEventEvent | ThreadEvent.Update; export type MatrixEventHandlerMap = { + /** + * Fires when an event is decrypted + * + * @param event - The matrix event which has been decrypted + * @param err - The error that occurred during decryption, or `undefined` if no error occurred. + */ [MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void; [MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void; [MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void; @@ -271,12 +279,43 @@ export class MatrixEvent extends TypedEventEmitterThis property is experimental and may change. + * @privateRemarks + * Should be read-only + */ + public forwardLooking = true; /* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event, * `Crypto` will set this the `VerificationRequest` for the event @@ -288,26 +327,11 @@ export class MatrixEvent extends TypedEventEmitterDo not access + * @param event - The raw (possibly encrypted) event. Do not access * this property directly unless you absolutely have to. Prefer the getter * methods defined on this class. Using the getter methods shields your app * from changes to event JSON between Matrix versions. - * - * @prop {RoomMember} sender The room member who sent this event, or null e.g. - * this is a presence event. This is only guaranteed to be set for events that - * appear in a timeline, ie. do not guarantee that it will be set on state - * events. - * @prop {RoomMember} target The room member who is the target of this event, e.g. - * the invitee, the person being banned, etc. - * @prop {EventStatus} status The sending status of the event. - * @prop {Error} error most recent error associated with sending the event, if any - * @prop {boolean} forwardLooking True if this event is 'forward looking', meaning - * that getDirectionalContent() will return event.content and not event.prev_content. - * Default: true. This property is experimental and may change. */ public constructor(public event: Partial = {}) { super(); @@ -318,19 +342,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; @@ -360,7 +384,7 @@ export class MatrixEvent extends TypedEventEmitter$143350589368169JsLZx:localhost + * @returns The event ID, e.g. $143350589368169JsLZx:localhost * */ public getId(): string | undefined { @@ -398,7 +422,7 @@ export class MatrixEvent extends TypedEventEmitter@alice:matrix.org + * @returns The user ID, e.g. `@alice:matrix.org` */ public getSender(): string | undefined { return this.event.sender || this.event.user_id; // v2 / v1 @@ -407,7 +431,7 @@ export class MatrixEvent extends TypedEventEmitterm.room.message + * @returns The event type, e.g. `m.room.message` */ public getType(): EventType | string { if (this.clearEvent) { @@ -420,16 +444,16 @@ export class MatrixEvent extends TypedEventEmitterundefined - * for m.presence events. - * @return {string?} The room ID, e.g. !cURbafjkfsMDVwdRDQ:matrix.org + * Get the room_id for this event. This will return `undefined` + * for `m.presence` events. + * @returns The room ID, e.g. !cURbafjkfsMDVwdRDQ:matrix.org * */ public getRoomId(): string | undefined { @@ -438,7 +462,7 @@ export class MatrixEvent extends TypedEventEmitter1433502692297 + * @returns The event timestamp, e.g. `1433502692297` */ public getTs(): number { return this.event.origin_server_ts!; @@ -446,7 +470,7 @@ export class MatrixEvent extends TypedEventEmitternew Date(1433502692297) + * @returns The event date, e.g. `new Date(1433502692297)` */ public getDate(): Date | null { return this.event.origin_server_ts ? new Date(this.event.origin_server_ts) : null; @@ -457,7 +481,11 @@ export class MatrixEvent extends TypedEventEmitter(): T { if (this._localRedactionEvent) { @@ -493,7 +521,7 @@ export class MatrixEvent extends TypedEventEmitter(): T { if (this._localRedactionEvent) { @@ -509,7 +537,7 @@ export class MatrixEvent extends TypedEventEmitterThis method is experimental and may change. - * @return {Object} event.content if this event is forward-looking, else + * @returns event.content if this event is forward-looking, else * event.prev_content. */ public getDirectionalContent(): IContent { @@ -585,7 +613,7 @@ export class MatrixEvent extends TypedEventEmitterundefined * for message events. - * @return {string} The event's state_key. + * @returns The event's `state_key`. */ public getStateKey(): string | undefined { return this.event.state_key; @@ -612,7 +640,7 @@ export class MatrixEvent extends TypedEventEmitter"m.room.encrypted" * - * @param {object} cryptoContent raw 'content' for the encrypted event. + * @param cryptoContent - raw 'content' for the encrypted event. * - * @param {string} senderCurve25519Key curve25519 key to record for the + * @param senderCurve25519Key - curve25519 key to record for the * sender of this event. - * See {@link module:models/event.MatrixEvent#getSenderKey}. + * See {@link MatrixEvent#getSenderKey}. * - * @param {string} claimedEd25519Key claimed ed25519 key to record for the + * @param claimedEd25519Key - claimed ed25519 key to record for the * sender if this event. - * See {@link module:models/event.MatrixEvent#getClaimedEd25519Key} + * See {@link MatrixEvent#getClaimedEd25519Key} */ public makeEncrypted( cryptoType: string, @@ -657,7 +685,7 @@ export class MatrixEvent extends TypedEventEmitter { @@ -741,10 +764,10 @@ export class MatrixEvent extends TypedEventEmitter { const wireContent = this.getWireContent(); @@ -759,9 +782,9 @@ export class MatrixEvent extends TypedEventEmitter} */ public getKeysClaimed(): Partial> { if (!this.claimedEd25519Key) return {}; @@ -982,8 +1001,6 @@ export class MatrixEvent extends TypedEventEmitter

When an event is first transmitted, a temporary copy of the event is + * inserted into the timeline, with a temporary event id, and a status of + * 'SENDING'. + * + *

Once the echo comes back from the server, the content of the event + * (MatrixEvent.event) is replaced by the complete event from the homeserver, + * thus updating its event id, as well as server-generated fields such as the + * timestamp. Its status is set to null. + * + *

Once the /send request completes, if the remote echo has not already + * arrived, the event is updated with a new event id and the status is set to + * 'SENT'. The server-generated fields are of course not updated yet. + * + *

If the /send fails, In this case, the event's status is set to + * 'NOT_SENT'. If it is later resent, the process starts again, setting the + * status to 'SENDING'. Alternatively, the message may be cancelled, which + * removes the event from the room, and sets the status to 'CANCELLED'. + * + *

This event is raised to reflect each of the transitions above. + * + * @param event - The matrix event which has been updated + * + * @param room - The room containing the redacted event + * + * @param oldEventId - The previous event id (the temporary event id, + * except when updating a successfully-sent event when its echo arrives) + * + * @param oldStatus - The previous event status. + */ [RoomEvent.LocalEchoUpdated]: ( event: MatrixEvent, room: Room, @@ -201,6 +311,7 @@ export class Room extends ReadReceipt { private txnToEvent: Record = {}; // Pending in-flight requests { string: MatrixEvent } private notificationCounts: NotificationCount = {}; private readonly threadNotifications = new Map(); + public readonly cachedThreadReadReceipts = new Map(); private readonly timelineSets: EventTimelineSet[]; public readonly threadsTimelineSets: EventTimelineSet[] = []; // any filtered timeline sets we're maintaining for this room @@ -227,7 +338,7 @@ export class Room extends ReadReceipt { public normalizedName: string; /** * Dict of room tags; the keys are the tag name and the values - * are any metadata associated with the tag - e.g. { "fav" : { order: 1 } } + * are any metadata associated with the tag - e.g. `{ "fav" : { order: 1 } }` */ public tags: Record> = {}; // $tagName: { $metadata: $value } /** @@ -301,21 +412,10 @@ export class Room extends ReadReceipt { *

In order that we can find events from their ids later, we also maintain a * map from event_id to timeline and index. * - * @constructor - * @alias module:models/room - * @param {string} roomId Required. The ID of this room. - * @param {MatrixClient} client Required. The client, used to lazy load members. - * @param {string} myUserId Required. The ID of the syncing user. - * @param {Object=} opts Configuration options - * - * @param {String=} opts.pendingEventOrdering Controls where pending messages - * appear in a room's timeline. If "chronological", messages will appear - * in the timeline when the call to sendEvent was made. If - * "detached", pending messages will appear in a separate list, - * accessible via {@link module:models/room#getPendingEvents}. Default: - * "chronological". - * @param {boolean} [opts.timelineSupport = false] Set to true to enable improved - * timeline support. + * @param roomId - Required. The ID of this room. + * @param client - Required. The client, used to lazy load members. + * @param myUserId - Required. The ID of the syncing user. + * @param opts - Configuration options */ public constructor( public readonly roomId: string, @@ -402,7 +502,7 @@ export class Room extends ReadReceipt { * - Last event of every room (to generate likely message preview) * - All events up to the read receipt (to calculate an accurate notification count) * - * @returns {Promise} Signals when all events have been decrypted + * @returns Signals when all events have been decrypted */ public async decryptCriticalEvents(): Promise { if (!this.client.isCryptoEnabled()) return; @@ -425,7 +525,7 @@ export class Room extends ReadReceipt { /** * Bulk decrypt events in a room * - * @returns {Promise} Signals when all events have been decrypted + * @returns Signals when all events have been decrypted */ public async decryptAllEvents(): Promise { if (!this.client.isCryptoEnabled()) return; @@ -443,7 +543,7 @@ export class Room extends ReadReceipt { /** * Gets the creator of the room - * @returns {string} The creator of the room, or null if it could not be determined + * @returns The creator of the room, or null if it could not be determined */ public getCreator(): string | null { const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, ""); @@ -452,7 +552,7 @@ export class Room extends ReadReceipt { /** * Gets the version of the room - * @returns {string} The version of the room, or null if it could not be determined + * @returns The version of the room, or null if it could not be determined */ public getVersion(): string { const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, ""); @@ -468,7 +568,7 @@ export class Room extends ReadReceipt { /** * Determines whether this room needs to be upgraded to a new version - * @returns {string?} What version the room should be upgraded to, or null if + * @returns What version the room should be upgraded to, or null if * the room does not require upgrading at this time. * @deprecated Use #getRecommendedVersion() instead */ @@ -489,13 +589,13 @@ export class Room extends ReadReceipt { /** * Determines the recommended room version for the room. This returns an - * object with 3 properties: version as the new version the + * object with 3 properties: `version` as the new version the * room should be upgraded to (may be the same as the current version); - * needsUpgrade to indicate if the room actually can be - * upgraded (ie: does the current version not match?); and urgent + * `needsUpgrade` to indicate if the room actually can be + * upgraded (ie: does the current version not match?); and `urgent` * to indicate if the new version patches a vulnerability in a previous * version. - * @returns {Promise<{version: string, needsUpgrade: boolean, urgent: boolean}>} + * @returns * Resolves to the version the room should be upgraded to. */ public async getRecommendedVersion(): Promise { @@ -576,8 +676,8 @@ export class Room extends ReadReceipt { /** * Determines whether the given user is permitted to perform a room upgrade - * @param {String} userId The ID of the user to test against - * @returns {boolean} True if the given user is permitted to upgrade the room + * @param userId - The ID of the user to test against + * @returns True if the given user is permitted to upgrade the room */ public userMayUpgradeRoom(userId: string): boolean { return this.currentState.maySendStateEvent(EventType.RoomTombstone, userId); @@ -586,10 +686,10 @@ export class Room extends ReadReceipt { /** * Get the list of pending sent events for this room * - * @return {module:models/event.MatrixEvent[]} A list of the sent events + * @returns A list of the sent events * waiting for remote echo. * - * @throws If opts.pendingEventOrdering was not 'detached' + * @throws If `opts.pendingEventOrdering` was not 'detached' */ public getPendingEvents(): MatrixEvent[] { if (!this.pendingEventList) { @@ -604,8 +704,7 @@ export class Room extends ReadReceipt { /** * Removes a pending event for this room * - * @param {string} eventId - * @return {boolean} True if an element was removed. + * @returns True if an element was removed. */ public removePendingEvent(eventId: string): boolean { if (!this.pendingEventList) { @@ -630,8 +729,7 @@ export class Room extends ReadReceipt { * Check whether the pending event list contains a given event by ID. * If pending event ordering is not "detached" then this returns false. * - * @param {string} eventId The event ID to check for. - * @return {boolean} + * @param eventId - The event ID to check for. */ public hasPendingEvent(eventId: string): boolean { return this.pendingEventList?.some(event => event.getId() === eventId) ?? false; @@ -640,8 +738,7 @@ export class Room extends ReadReceipt { /** * Get a specific event from the pending event list, if configured, null otherwise. * - * @param {string} eventId The event ID to check for. - * @return {MatrixEvent} + * @param eventId - The event ID to check for. */ public getPendingEvent(eventId: string): MatrixEvent | null { return this.pendingEventList?.find(event => event.getId() === eventId) ?? null; @@ -650,7 +747,7 @@ export class Room extends ReadReceipt { /** * Get the live unfiltered timeline for this room. * - * @return {module:models/event-timeline~EventTimeline} live timeline + * @returns live timeline */ public getLiveTimeline(): EventTimeline { return this.getUnfilteredTimelineSet().getLiveTimeline(); @@ -659,7 +756,7 @@ export class Room extends ReadReceipt { /** * Get the timestamp of the last message in the room * - * @return {number} the timestamp of the last message in the room + * @returns the timestamp of the last message in the room */ public getLastActiveTimestamp(): number { const timeline = this.getLiveTimeline(); @@ -673,7 +770,7 @@ export class Room extends ReadReceipt { } /** - * @return {string} the membership type (join | leave | invite) for the logged in user + * @returns the membership type (join | leave | invite) for the logged in user */ public getMyMembership(): string { return this.selfMembership ?? "leave"; @@ -682,7 +779,7 @@ export class Room extends ReadReceipt { /** * If this room is a DM we're invited to, * try to find out who invited us - * @return {string} user id of the inviter + * @returns user id of the inviter */ public getDMInviter(): string | undefined { const me = this.getMember(this.myUserId); @@ -701,7 +798,7 @@ export class Room extends ReadReceipt { /** * Assuming this room is a DM room, tries to guess with which user. - * @return {string} user id of the other member (could be syncing user) + * @returns user id of the other member (could be syncing user) */ public guessDMUserId(): string { const me = this.getMember(this.myUserId); @@ -768,7 +865,7 @@ export class Room extends ReadReceipt { /** * Sets the membership this room was received as during sync - * @param {string} membership join | leave | invite + * @param membership - join | leave | invite */ public updateMyMembership(membership: string): void { const prevMembership = this.selfMembership; @@ -812,7 +909,7 @@ export class Room extends ReadReceipt { * Preloads the member list in case lazy loading * of memberships is in use. Can be called multiple times, * it will only preload once. - * @return {Promise} when preloading is done and + * @returns when preloading is done and * accessing the members on the room will take * all members in the room into account */ @@ -893,7 +990,7 @@ export class Room extends ReadReceipt { /** * Empty out the current live timeline and re-request it. This is used when - * historical messages are imported into the room via MSC2716 `/batch_send + * historical messages are imported into the room via MSC2716 `/batch_send` * because the client may already have that section of the timeline loaded. * We need to force the client to throw away their current timeline so that * when they back paginate over the area again with the historical messages @@ -998,8 +1095,8 @@ export class Room extends ReadReceipt { * *

This is used when /sync returns a 'limited' timeline. * - * @param {string=} backPaginationToken token for back-paginating the new timeline - * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline, + * @param backPaginationToken - token for back-paginating the new timeline + * @param forwardPaginationToken - token for forward-paginating the old live timeline, * if absent or null, all timelines are reset, removing old ones (including the previous live * timeline which would otherwise be unable to paginate forwards without this token). * Removing just the old live timeline whilst preserving previous ones is not supported. @@ -1018,7 +1115,7 @@ export class Room extends ReadReceipt { /** * Fix up this.timeline, this.oldState and this.currentState * - * @private + * @internal */ private fixUpLegacyTimelineFields(): void { const previousOldState = this.oldState; @@ -1077,7 +1174,7 @@ export class Room extends ReadReceipt { * disabled, then we aren't tracking room devices at all, so we can't answer this, and an * error will be thrown. * - * @return {boolean} the result + * @returns the result */ public async hasUnverifiedDevices(): Promise { if (!this.client.isRoomEncrypted(this.roomId)) { @@ -1095,7 +1192,7 @@ export class Room extends ReadReceipt { /** * Return the timeline sets for this room. - * @return {EventTimelineSet[]} array of timeline sets for this room + * @returns array of timeline sets for this room */ public getTimelineSets(): EventTimelineSet[] { return this.timelineSets; @@ -1103,7 +1200,7 @@ export class Room extends ReadReceipt { /** * Helper to return the main unfiltered timeline set for this room - * @return {EventTimelineSet} room's unfiltered timeline set + * @returns room's unfiltered timeline set */ public getUnfilteredTimelineSet(): EventTimelineSet { return this.timelineSets[0]; @@ -1112,8 +1209,8 @@ export class Room extends ReadReceipt { /** * Get the timeline which contains the given event from the unfiltered set, if any * - * @param {string} eventId event ID to look for - * @return {?module:models/event-timeline~EventTimeline} timeline containing + * @param eventId - event ID to look for + * @returns timeline containing * the given event, or null if unknown */ public getTimelineForEvent(eventId: string): EventTimeline | null { @@ -1129,7 +1226,7 @@ export class Room extends ReadReceipt { /** * Add a new timeline to this room's unfiltered timeline set * - * @return {module:models/event-timeline~EventTimeline} newly-created timeline + * @returns newly-created timeline */ public addTimeline(): EventTimeline { return this.getUnfilteredTimelineSet().addTimeline(); @@ -1138,7 +1235,7 @@ export class Room extends ReadReceipt { /** * Whether the timeline needs to be refreshed in order to pull in new * historical messages that were imported. - * @param {Boolean} value The value to set + * @param value - The value to set */ public setTimelineNeedsRefresh(value: boolean): void { this.timelineNeedsRefresh = value; @@ -1147,7 +1244,7 @@ export class Room extends ReadReceipt { /** * Whether the timeline needs to be refreshed in order to pull in new * historical messages that were imported. - * @return {Boolean} . + * @returns . */ public getTimelineNeedsRefresh(): boolean { return this.timelineNeedsRefresh; @@ -1156,8 +1253,8 @@ export class Room extends ReadReceipt { /** * Get an event which is stored in our unfiltered timeline set, or in a thread * - * @param {string} eventId event ID to look for - * @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown + * @param eventId - event ID to look for + * @returns the given event, or undefined if unknown */ public findEventById(eventId: string): MatrixEvent | undefined { let event = this.getUnfilteredTimelineSet().findEventById(eventId); @@ -1178,12 +1275,12 @@ export class Room extends ReadReceipt { /** * Get one of the notification counts for this room - * @param {String} type The type of notification count to get. default: 'total' - * @return {Number} The notification count, or undefined if there is no count + * @param type - The type of notification count to get. default: 'total' + * @returns The notification count, or undefined if there is no count * for this type. */ public getUnreadNotificationCount(type = NotificationCountType.Total): number { - let count = this.notificationCounts[type] ?? 0; + let count = this.getRoomUnreadNotificationCount(type); if (this.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) { for (const threadNotification of this.threadNotifications.values()) { count += threadNotification[type] ?? 0; @@ -1192,11 +1289,32 @@ export class Room extends ReadReceipt { return count; } + /** + * Get the notification for the event context (room or thread timeline) + */ + public getUnreadCountForEventContext(type = NotificationCountType.Total, event: MatrixEvent): number { + const isThreadEvent = !!event.threadRootId && !event.isThreadRoot; + + return (isThreadEvent + ? this.getThreadUnreadNotificationCount(event.threadRootId, type) + : this.getRoomUnreadNotificationCount(type)) ?? 0; + } + + /** + * Get one of the notification counts for this room + * @param type - The type of notification count to get. default: 'total' + * @returns The notification count, or undefined if there is no count + * for this type. + */ + public getRoomUnreadNotificationCount(type = NotificationCountType.Total): number { + return this.notificationCounts[type] ?? 0; + } + /** * @experimental * Get one of the notification counts for a thread - * @param threadId the root event ID - * @param type The type of notification count to get. default: 'total' + * @param threadId - the root event ID + * @param type - The type of notification count to get. default: 'total' * @returns The notification count, or undefined if there is no count * for this type. */ @@ -1207,7 +1325,7 @@ export class Room extends ReadReceipt { /** * @experimental * Checks if the current room has unread thread notifications - * @returns {boolean} + * @returns */ public hasThreadUnreadNotification(): boolean { for (const notification of this.threadNotifications.values()) { @@ -1221,9 +1339,9 @@ export class Room extends ReadReceipt { /** * @experimental * Swet one of the notification count for a thread - * @param threadId the root event ID - * @param type The type of notification count to get. default: 'total' - * @returns {void} + * @param threadId - the root event ID + * @param type - The type of notification count to get. default: 'total' + * @returns */ public setThreadUnreadNotificationCount(threadId: string, type: NotificationCountType, count: number): void { const notification: NotificationCount = { @@ -1278,8 +1396,8 @@ export class Room extends ReadReceipt { /** * Set one of the notification counts for this room - * @param {String} type The type of notification count to set. - * @param {Number} count The new count + * @param type - The type of notification count to set. + * @param count - The new count */ public setUnreadNotificationCount(type: NotificationCountType, count: number): void { this.notificationCounts[type] = count; @@ -1291,10 +1409,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, @@ -1308,7 +1426,7 @@ export class Room extends ReadReceipt { /** * Whether to send encrypted messages to devices within this room. - * @param {Boolean} value true to blacklist unverified devices, null + * @param value - true to blacklist unverified devices, null * to use the global value for this room. */ public setBlacklistUnverifiedDevices(value: boolean): void { @@ -1317,7 +1435,7 @@ export class Room extends ReadReceipt { /** * Whether to send encrypted messages to devices within this room. - * @return {Boolean} true if blacklisting unverified devices, null + * @returns true if blacklisting unverified devices, null * if the global value should be used for this room. */ public getBlacklistUnverifiedDevices(): boolean | null { @@ -1327,15 +1445,15 @@ export class Room extends ReadReceipt { /** * Get the avatar URL for a room if one was set. - * @param {String} baseUrl The homeserver base URL. See - * {@link module:client~MatrixClient#getHomeserverUrl}. - * @param {Number} width The desired width of the thumbnail. - * @param {Number} height The desired height of the thumbnail. - * @param {string} resizeMethod The thumbnail resize method to use, either + * @param baseUrl - The homeserver base URL. See + * {@link MatrixClient#getHomeserverUrl}. + * @param width - The desired width of the thumbnail. + * @param height - The desired height of the thumbnail. + * @param resizeMethod - The thumbnail resize method to use, either * "crop" or "scale". - * @param {boolean} allowDefault True to allow an identicon for this room if an + * @param allowDefault - True to allow an identicon for this room if an * avatar URL wasn't explicitly set. Default: true. (Deprecated) - * @return {?string} the avatar URL or null. + * @returns the avatar URL or null. */ public getAvatarUrl( baseUrl: string, @@ -1359,7 +1477,7 @@ export class Room extends ReadReceipt { /** * Get the mxc avatar url for the room, if one was set. - * @return {string} the mxc avatar url or falsy + * @returns the mxc avatar url or falsy */ public getMxcAvatarUrl(): string | null { return this.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url || null; @@ -1369,7 +1487,7 @@ export class Room extends ReadReceipt { * Get this room's canonical alias * The alias returned by this function may not necessarily * still point to this room. - * @return {?string} The room's canonical alias, or null if there is none + * @returns The room's canonical alias, or null if there is none */ public getCanonicalAlias(): string | null { const canonicalAlias = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, ""); @@ -1381,7 +1499,7 @@ export class Room extends ReadReceipt { /** * Get this room's alternative aliases - * @return {array} The room's alternative aliases, or an empty array + * @returns The room's alternative aliases, or an empty array */ public getAltAliases(): string[] { const canonicalAlias = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, ""); @@ -1396,19 +1514,19 @@ export class Room extends ReadReceipt { * *

Will fire "Room.timeline" for each event added. * - * @param {MatrixEvent[]} events A list of events to add. + * @param events - A list of events to add. * - * @param {boolean} toStartOfTimeline True to add these events to the start + * @param toStartOfTimeline - True to add these events to the start * (oldest) instead of the end (newest) of the timeline. If true, the oldest * event will be the last element of 'events'. * - * @param {module:models/event-timeline~EventTimeline} timeline timeline to + * @param timeline - timeline to * add events to. * - * @param {string=} paginationToken token for the next batch of events - * - * @fires module:client~MatrixClient#event:"Room.timeline" + * @param paginationToken - token for the next batch of events * + * @remarks + * Fires {@link RoomEvent.Timeline} */ public addEventsToTimeline( events: MatrixEvent[], @@ -1435,8 +1553,8 @@ export class Room extends ReadReceipt { /** * Get a member from the current room state. - * @param {string} userId The user ID of the member. - * @return {RoomMember} The member or null. + * @param userId - The user ID of the member. + * @returns The member or `null`. */ public getMember(userId: string): RoomMember | null { return this.currentState.getMember(userId); @@ -1445,7 +1563,7 @@ export class Room extends ReadReceipt { /** * Get all currently loaded members from the current * room state. - * @returns {RoomMember[]} Room members + * @returns Room members */ public getMembers(): RoomMember[] { return this.currentState.getMembers(); @@ -1453,7 +1571,7 @@ export class Room extends ReadReceipt { /** * Get a list of members whose membership state is "join". - * @return {RoomMember[]} A list of currently joined members. + * @returns A list of currently joined members. */ public getJoinedMembers(): RoomMember[] { return this.getMembersWithMembership("join"); @@ -1464,7 +1582,7 @@ export class Room extends ReadReceipt { * This method caches the result. * This is a wrapper around the method of the same name in roomState, returning * its result for the room's current state. - * @return {number} The number of members in this room whose membership is 'join' + * @returns The number of members in this room whose membership is 'join' */ public getJoinedMemberCount(): number { return this.currentState.getJoinedMemberCount(); @@ -1472,7 +1590,7 @@ export class Room extends ReadReceipt { /** * Returns the number of invited members in this room - * @return {number} The number of members in this room whose membership is 'invite' + * @returns The number of members in this room whose membership is 'invite' */ public getInvitedMemberCount(): number { return this.currentState.getInvitedMemberCount(); @@ -1480,7 +1598,7 @@ export class Room extends ReadReceipt { /** * Returns the number of invited + joined members in this room - * @return {number} The number of members in this room whose membership is 'invite' or 'join' + * @returns The number of members in this room whose membership is 'invite' or 'join' */ public getInvitedAndJoinedMemberCount(): number { return this.getInvitedMemberCount() + this.getJoinedMemberCount(); @@ -1488,8 +1606,8 @@ export class Room extends ReadReceipt { /** * Get a list of members with given membership state. - * @param {string} membership The membership state. - * @return {RoomMember[]} A list of members with the given membership state. + * @param membership - The membership state. + * @returns A list of members with the given membership state. */ public getMembersWithMembership(membership: string): RoomMember[] { return this.currentState.getMembers().filter(function(m) { @@ -1499,7 +1617,7 @@ export class Room extends ReadReceipt { /** * Get a list of members we should be encrypting for in this room - * @return {Promise} A list of members who + * @returns A list of members who * we should encrypt messages for in this room. */ public async getEncryptionTargetMembers(): Promise { @@ -1513,7 +1631,7 @@ export class Room extends ReadReceipt { /** * Determine whether we should encrypt messages for invited users in this room - * @return {boolean} if we should encrypt messages for invited users + * @returns if we should encrypt messages for invited users */ public shouldEncryptForInvitedMembers(): boolean { const ev = this.currentState.getStateEvents(EventType.RoomHistoryVisibility, ""); @@ -1523,9 +1641,9 @@ export class Room extends ReadReceipt { /** * Get the default room name (i.e. what a given user would see if the * room had no m.room.name) - * @param {string} userId The userId from whose perspective we want + * @param userId - The userId from whose perspective we want * to calculate the default name - * @return {string} The default room name + * @returns The default room name */ public getDefaultRoomName(userId: string): string { return this.calculateRoomName(userId, true); @@ -1533,9 +1651,9 @@ export class Room extends ReadReceipt { /** * Check if the given user_id has the given membership state. - * @param {string} userId The user ID to check. - * @param {string} membership The membership e.g. 'join' - * @return {boolean} True if this user_id has the given membership state. + * @param userId - The user ID to check. + * @param membership - The membership e.g. `'join'` + * @returns True if this user_id has the given membership state. */ public hasMembershipState(userId: string, membership: string): boolean { const member = this.getMember(userId); @@ -1547,9 +1665,9 @@ export class Room extends ReadReceipt { /** * Add a timelineSet for this room with the given filter - * @param {Filter} filter The filter to be applied to this timelineSet - * @param {Object=} opts Configuration options - * @return {EventTimelineSet} The timelineSet + * @param filter - The filter to be applied to this timelineSet + * @param opts - Configuration options + * @returns The timelineSet */ public getOrCreateFilteredTimelineSet( filter: Filter, @@ -1675,7 +1793,7 @@ export class Room extends ReadReceipt { Array.from(this.threads) .forEach(([, thread]) => { if (thread.length === 0) return; - const currentUserParticipated = thread.events.some(event => { + const currentUserParticipated = thread.timeline.some(event => { return event.getSender() === this.client.getUserId(); }); if (filterType !== ThreadFilterType.My || currentUserParticipated) { @@ -1693,8 +1811,6 @@ export class Room extends ReadReceipt { /** * Takes the given thread root events and creates threads for them. - * @param events - * @param toStartOfTimeline */ public processThreadRoots(events: MatrixEvent[], toStartOfTimeline: boolean): void { for (const rootEvent of events) { @@ -1758,20 +1874,17 @@ export class Room extends ReadReceipt { let latestMyThreadsRootEvent: MatrixEvent | undefined; const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); for (const rootEvent of threadRoots) { - this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, { + const opts = { duplicateStrategy: DuplicateStrategy.Ignore, fromCache: false, roomState, - }); + }; + this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts); const threadRelationship = rootEvent .getServerAggregatedRelation(THREAD_RELATION_TYPE.name); if (threadRelationship?.current_user_participated) { - this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, { - duplicateStrategy: DuplicateStrategy.Ignore, - fromCache: false, - roomState, - }); + this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, opts); latestMyThreadsRootEvent = rootEvent; } } @@ -1791,8 +1904,7 @@ export class Room extends ReadReceipt { /** * Fetch a single page of threadlist messages for the specific thread filter - * @param filter - * @private + * @internal */ private async fetchRoomThreadList(filter?: ThreadFilterType): Promise { const timelineSet = filter === ThreadFilterType.My @@ -1808,7 +1920,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; @@ -1825,7 +1937,7 @@ export class Room extends ReadReceipt { } private onThreadNewReply(thread: Thread): void { - this.updateThreadRootEvents(thread, false); + this.updateThreadRootEvents(thread, false, true); } private onThreadDelete(thread: Thread): void { @@ -1846,7 +1958,7 @@ export class Room extends ReadReceipt { /** * Forget the timelineSet for this room with the given filter * - * @param {Filter} filter the filter whose timelineSet is to be forgotten + * @param filter - the filter whose timelineSet is to be forgotten */ public removeFilteredTimelineSet(filter: Filter): void { const timelineSet = this.filteredTimelineSets[filter.filterId!]; @@ -1950,11 +2062,11 @@ export class Room extends ReadReceipt { )); } - private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean): void => { + private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean, recreateEvent: boolean): void => { if (thread.length) { - this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline); + this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline, recreateEvent); if (thread.hasCurrentUserParticipated) { - this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline); + this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline, recreateEvent); } } }; @@ -1963,8 +2075,12 @@ export class Room extends ReadReceipt { timelineSet: Optional, thread: Thread, toStartOfTimeline: boolean, + recreateEvent: boolean, ): void => { if (timelineSet && thread.rootEvent) { + if (recreateEvent) { + timelineSet.removeEvent(thread.id); + } if (Thread.hasServerSideSupport) { timelineSet.addLiveEvent(thread.rootEvent, { duplicateStrategy: DuplicateStrategy.Replace, @@ -1999,6 +2115,7 @@ export class Room extends ReadReceipt { const thread = new Thread(threadId, rootEvent, { room: this, client: this.client, + pendingEventOrdering: this.opts.pendingEventOrdering, }); // This is necessary to be able to jump to events in threads: @@ -2027,7 +2144,17 @@ export class Room extends ReadReceipt { } if (this.threadsReady) { - this.updateThreadRootEvents(thread, toStartOfTimeline); + this.updateThreadRootEvents(thread, toStartOfTimeline, false); + } + + // Pulling all the cached thread read receipts we've discovered when we + // did an initial sync, and applying them to the thread now that it exists + // on the client side + if (this.cachedThreadReadReceipts.has(threadId)) { + for (const { event, synthetic } of this.cachedThreadReadReceipts.get(threadId)!) { + this.addReceipt(event, synthetic); + } + this.cachedThreadReadReceipts.delete(threadId); } this.emit(ThreadEvent.New, thread, toStartOfTimeline); @@ -2119,10 +2246,12 @@ export class Room extends ReadReceipt { * Add an event to the end of this room's live timelines. Will fire * "Room.timeline". * - * @param {MatrixEvent} event Event to be added - * @param {IAddLiveEventOptions} addLiveEventOptions addLiveEvent options - * @fires module:client~MatrixClient#event:"Room.timeline" - * @private + * @param event - Event to be added + * @param addLiveEventOptions - addLiveEvent options + * @internal + * + * @remarks + * Fires {@link RoomEvent.Timeline} */ private addLiveEvent(event: MatrixEvent, addLiveEventOptions: IAddLiveEventOptions): void { const { duplicateStrategy, timelineWasEmpty, fromCache } = addLiveEventOptions; @@ -2162,14 +2291,15 @@ export class Room extends ReadReceipt { * *

This is an internal method, intended for use by MatrixClient. * - * @param {module:models/event.MatrixEvent} event The event to add. + * @param event - The event to add. * - * @param {string} txnId Transaction id for this outgoing event - * - * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + * @param txnId - Transaction id for this outgoing event * * @throws if the event doesn't have status SENDING, or we aren't given a * unique transaction id. + * + * @remarks + * Fires {@link RoomEvent.LocalEchoUpdated} */ public addPendingEvent(event: MatrixEvent, txnId: string): void { if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) { @@ -2241,7 +2371,7 @@ export class Room extends ReadReceipt { * all messages that are not yet encrypted will be discarded * * This is because the flow of EVENT_STATUS transition is - * queued => sending => encrypting => sending => sent + * `queued => sending => encrypting => sending => sent` * * Steps 3 and 4 are skipped for unencrypted room. * It is better to discard an unencrypted message rather than persisting @@ -2273,7 +2403,7 @@ export class Room extends ReadReceipt { * which are just kept detached for their local echo. * * Also note that live events are aggregated in the live EventTimelineSet. - * @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated. + * @param event - the relation event that needs to be aggregated. */ private aggregateNonLiveRelation(event: MatrixEvent): void { this.relations.aggregateChildEvent(event); @@ -2289,13 +2419,15 @@ export class Room extends ReadReceipt { *

We move the event to the live timeline if it isn't there already, and * update it. * - * @param {module:models/event.MatrixEvent} remoteEvent The event received from + * @param remoteEvent - The event received from * /sync - * @param {module:models/event.MatrixEvent} localEvent The local echo, which + * @param localEvent - The local echo, which * should be either in the pendingEventList or the timeline. * - * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" - * @private + * @internal + * + * @remarks + * Fires {@link RoomEvent.LocalEchoUpdated} */ public handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void { const oldEventId = localEvent.getId()!; @@ -2336,11 +2468,12 @@ export class Room extends ReadReceipt { * *

This is an internal method. * - * @param {MatrixEvent} event local echo event - * @param {EventStatus} newStatus status to assign - * @param {string} newEventId new event id to assign. Ignored unless - * newStatus == EventStatus.SENT. - * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + * @param event - local echo event + * @param newStatus - status to assign + * @param newEventId - new event id to assign. Ignored unless newStatus == EventStatus.SENT. + * + * @remarks + * Fires {@link RoomEvent.LocalEchoUpdated} */ public updatePendingEvent(event: MatrixEvent, newStatus: EventStatus, newEventId?: string): void { logger.log( @@ -2447,9 +2580,9 @@ export class Room extends ReadReceipt { * events and typing notifications. These events are treated as "live" so * they will go to the end of the timeline. * - * @param {MatrixEvent[]} events A list of events to add. - * @param {IAddLiveEventOptions} addLiveEventOptions addLiveEvent options - * @throws If duplicateStrategy is not falsey, 'replace' or 'ignore'. + * @param events - A list of events to add. + * @param addLiveEventOptions - addLiveEvent options + * @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'. */ public addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): void; /** @@ -2592,8 +2725,8 @@ export class Room extends ReadReceipt { /** * Add a receipt event to the room. - * @param {MatrixEvent} event The m.receipt event. - * @param {Boolean} synthetic True if this event is implicit. + * @param event - The m.receipt event. + * @param synthetic - True if this event is implicit. */ public addReceipt(event: MatrixEvent, synthetic = false): void { const content = event.getContent(); @@ -2605,13 +2738,24 @@ export class Room extends ReadReceipt { const receiptDestination: Thread | this | undefined = receiptForMainTimeline ? this : this.threads.get(receipt.thread_id ?? ""); - receiptDestination?.addReceiptToStructure( - eventId, - receiptType as ReceiptType, - userId, - receipt, - synthetic, - ); + + if (receiptDestination) { + receiptDestination.addReceiptToStructure( + eventId, + receiptType as ReceiptType, + userId, + receipt, + synthetic, + ); + } else { + // The thread does not exist locally, keep the read receipt + // in a cache locally, and re-apply the `addReceipt` logic + // when the thread is created + this.cachedThreadReadReceipts.set(receipt.thread_id!, [ + ...(this.cachedThreadReadReceipts.get(receipt.thread_id!) ?? []), + { event, synthetic }, + ]); + } }); }); }); @@ -2623,7 +2767,7 @@ export class Room extends ReadReceipt { /** * Adds/handles ephemeral events such as typing notifications and read receipts. - * @param {MatrixEvent[]} events A list of events to process + * @param events - A list of events to process */ public addEphemeralEvents(events: MatrixEvent[]): void { for (const event of events) { @@ -2637,7 +2781,7 @@ export class Room extends ReadReceipt { /** * Removes events from this room. - * @param {String[]} eventIds A list of eventIds to remove. + * @param eventIds - A list of eventIds to remove. */ public removeEvents(eventIds: string[]): void { for (const eventId of eventIds) { @@ -2648,9 +2792,9 @@ export class Room extends ReadReceipt { /** * Removes a single event from this room. * - * @param {String} eventId The id of the event to remove + * @param eventId - The id of the event to remove * - * @return {boolean} true if the event was removed from any of the room's timeline sets + * @returns true if the event was removed from any of the room's timeline sets */ public removeEvent(eventId: string): boolean { let removedAny = false; @@ -2670,7 +2814,9 @@ export class Room extends ReadReceipt { * Recalculate various aspects of the room, including the room name and * room summary. Call this any time the room's current state is modified. * May fire "Room.name" if the room name is updated. - * @fires module:client~MatrixClient#event:"Room.name" + * + * @remarks + * Fires {@link RoomEvent.Name} */ public recalculate(): void { // set fake stripped state events if this is an invite room so logic remains @@ -2713,7 +2859,7 @@ export class Room extends ReadReceipt { /** * Update the room-tag event for the room. The previous one is overwritten. - * @param {MatrixEvent} event the m.tag event + * @param event - the m.tag event */ public addTags(event: MatrixEvent): void { // event content looks like: @@ -2734,7 +2880,7 @@ export class Room extends ReadReceipt { /** * Update the account_data events for this room, overwriting events of the same type. - * @param {Array} events an array of account_data events to add + * @param events - an array of account_data events to add */ public addAccountData(events: MatrixEvent[]): void { for (const event of events) { @@ -2749,8 +2895,8 @@ export class Room extends ReadReceipt { /** * Access account_data event of given event type for this room - * @param {string} type the type of account_data event to be accessed - * @return {?MatrixEvent} the account_data event in question + * @param type - the type of account_data event to be accessed + * @returns the account_data event in question */ public getAccountData(type: EventType | string): MatrixEvent | undefined { return this.accountData[type]; @@ -2758,7 +2904,7 @@ export class Room extends ReadReceipt { /** * Returns whether the syncing user has permission to send a message in the room - * @return {boolean} true if the user should be permitted to send + * @returns true if the user should be permitted to send * message events into the room. */ public maySendMessage(): boolean { @@ -2769,8 +2915,8 @@ export class Room extends ReadReceipt { /** * Returns whether the given user has permissions to issue an invite for this room. - * @param {string} userId the ID of the Matrix user to check permissions for - * @returns {boolean} true if the user should be permitted to issue invites for this room. + * @param userId - the ID of the Matrix user to check permissions for + * @returns true if the user should be permitted to issue invites for this room. */ public canInvite(userId: string): boolean { let canInvite = this.getMyMembership() === "join"; @@ -2785,7 +2931,7 @@ export class Room extends ReadReceipt { /** * Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`. - * @returns {string} the join_rule applied to this room + * @returns the join_rule applied to this room */ public getJoinRule(): JoinRule { return this.currentState.getJoinRule(); @@ -2793,7 +2939,7 @@ export class Room extends ReadReceipt { /** * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. - * @returns {HistoryVisibility} the history_visibility applied to this room + * @returns the history_visibility applied to this room */ public getHistoryVisibility(): HistoryVisibility { return this.currentState.getHistoryVisibility(); @@ -2801,7 +2947,7 @@ export class Room extends ReadReceipt { /** * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. - * @returns {HistoryVisibility} the history_visibility applied to this room + * @returns the history_visibility applied to this room */ public getGuestAccess(): GuestAccess { return this.currentState.getGuestAccess(); @@ -2809,7 +2955,7 @@ export class Room extends ReadReceipt { /** * Returns the type of the room from the `m.room.create` event content or undefined if none is set - * @returns {?string} the type of the room. + * @returns the type of the room. */ public getType(): RoomType | string | undefined { const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, ""); @@ -2825,7 +2971,7 @@ export class Room extends ReadReceipt { /** * Returns whether the room is a space-room as defined by MSC1772. - * @returns {boolean} true if the room's type is RoomType.Space + * @returns true if the room's type is RoomType.Space */ public isSpaceRoom(): boolean { return this.getType() === RoomType.Space; @@ -2833,7 +2979,7 @@ export class Room extends ReadReceipt { /** * Returns whether the room is a call-room as defined by MSC3417. - * @returns {boolean} true if the room's type is RoomType.UnstableCall + * @returns true if the room's type is RoomType.UnstableCall */ public isCallRoom(): boolean { return this.getType() === RoomType.UnstableCall; @@ -2841,7 +2987,7 @@ export class Room extends ReadReceipt { /** * Returns whether the room is a video room. - * @returns {boolean} true if the room's type is RoomType.ElementVideo + * @returns true if the room's type is RoomType.ElementVideo */ public isElementVideoRoom(): boolean { return this.getType() === RoomType.ElementVideo; @@ -2877,11 +3023,11 @@ export class Room extends ReadReceipt { /** * This is an internal method. Calculates the name of the room from the current * room state. - * @param {string} userId The client's user ID. Used to filter room members + * @param userId - The client's user ID. Used to filter room members * correctly. - * @param {boolean} ignoreRoomNameEvent Return the implicit room name that we'd see if there + * @param ignoreRoomNameEvent - Return the implicit room name that we'd see if there * was no m.room.name event. - * @return {string} The calculated room name. + * @returns The calculated room name. */ private calculateRoomName(userId: string, ignoreRoomNameEvent = false): string { if (!ignoreRoomNameEvent) { @@ -3128,7 +3274,7 @@ export class Room extends ReadReceipt { * a (more recent) visibility change event, patch the event in * place so that clients now not to display it. * - * @param event Any matrix event. If this event has at least one a + * @param event - Any matrix event. If this event has at least one a * pending visibility change event, apply the latest visibility * change event. */ @@ -3227,118 +3373,3 @@ function memberNamesToRoomName(names: string[], count: number): string { } } } - -/** - * Fires when an event we had previously received is redacted. - * - * (Note this is *not* fired when the redaction happens before we receive the - * event). - * - * @event module:client~MatrixClient#"Room.redaction" - * @param {MatrixEvent} event The matrix redaction event - * @param {Room} room The room containing the redacted event - */ - -/** - * Fires when an event that was previously redacted isn't anymore. - * This happens when the redaction couldn't be sent and - * was subsequently cancelled by the user. Redactions have a local echo - * which is undone in this scenario. - * - * @event module:client~MatrixClient#"Room.redactionCancelled" - * @param {MatrixEvent} event The matrix redaction event that was cancelled. - * @param {Room} room The room containing the unredacted event - */ - -/** - * Fires whenever the name of a room is updated. - * @event module:client~MatrixClient#"Room.name" - * @param {Room} room The room whose Room.name was updated. - * @example - * matrixClient.on("Room.name", function(room){ - * var newName = room.name; - * }); - */ - -/** - * Fires whenever a receipt is received for a room - * @event module:client~MatrixClient#"Room.receipt" - * @param {event} event The receipt event - * @param {Room} room The room whose receipts was updated. - * @example - * matrixClient.on("Room.receipt", function(event, room){ - * var receiptContent = event.getContent(); - * }); - */ - -/** - * Fires whenever a room's tags are updated. - * @event module:client~MatrixClient#"Room.tags" - * @param {event} event The tags event - * @param {Room} room The room whose Room.tags was updated. - * @example - * matrixClient.on("Room.tags", function(event, room){ - * var newTags = event.getContent().tags; - * if (newTags["favourite"]) showStar(room); - * }); - */ - -/** - * Fires whenever a room's account_data is updated. - * @event module:client~MatrixClient#"Room.accountData" - * @param {event} event The account_data event - * @param {Room} room The room whose account_data was updated. - * @param {MatrixEvent} prevEvent The event being replaced by - * the new account data, if known. - * @example - * matrixClient.on("Room.accountData", function(event, room, oldEvent){ - * if (event.getType() === "m.room.colorscheme") { - * applyColorScheme(event.getContents()); - * } - * }); - */ - -/** - * Fires when the status of a transmitted event is updated. - * - *

When an event is first transmitted, a temporary copy of the event is - * inserted into the timeline, with a temporary event id, and a status of - * 'SENDING'. - * - *

Once the echo comes back from the server, the content of the event - * (MatrixEvent.event) is replaced by the complete event from the homeserver, - * thus updating its event id, as well as server-generated fields such as the - * timestamp. Its status is set to null. - * - *

Once the /send request completes, if the remote echo has not already - * arrived, the event is updated with a new event id and the status is set to - * 'SENT'. The server-generated fields are of course not updated yet. - * - *

If the /send fails, In this case, the event's status is set to - * 'NOT_SENT'. If it is later resent, the process starts again, setting the - * status to 'SENDING'. Alternatively, the message may be cancelled, which - * removes the event from the room, and sets the status to 'CANCELLED'. - * - *

This event is raised to reflect each of the transitions above. - * - * @event module:client~MatrixClient#"Room.localEchoUpdated" - * - * @param {MatrixEvent} event The matrix event which has been updated - * - * @param {Room} room The room containing the redacted event - * - * @param {string} oldEventId The previous event id (the temporary event id, - * except when updating a successfully-sent event when its echo arrives) - * - * @param {EventStatus} oldStatus The previous event status. - */ - -/** - * Fires when the logged in user's membership in the room is updated. - * - * @event module:models/room~Room#"Room.myMembership" - * @param {Room} room The room in which the membership has been updated - * @param {string} membership The new membership value - * @param {string} prevMembership The previous membership value - */ - diff --git a/src/models/search-result.ts b/src/models/search-result.ts index f598301d7..2f27495ac 100644 --- a/src/models/search-result.ts +++ b/src/models/search-result.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module models/search-result - */ - import { EventContext } from "./event-context"; import { EventMapper } from "../event-mapper"; import { IResultContext, ISearchResult } from "../@types/search"; @@ -25,10 +21,6 @@ import { IResultContext, ISearchResult } from "../@types/search"; export class SearchResult { /** * Create a SearchResponse from the response to /search - * @static - * @param {Object} jsonObj - * @param {function} eventMapper - * @return {SearchResult} */ public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult { @@ -54,11 +46,9 @@ export class SearchResult { /** * Construct a new SearchResult * - * @param {number} rank where this SearchResult ranks in the results - * @param {event-context.EventContext} context the matching event and its + * @param rank - where this SearchResult ranks in the results + * @param context - the matching event and its * context - * - * @constructor */ public constructor(public readonly rank: number, public readonly context: EventContext) {} } diff --git a/src/models/thread.ts b/src/models/thread.ts index 5abe63d6c..61b010a18 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -16,10 +16,10 @@ limitations under the License. import { Optional } from "matrix-events-sdk"; -import { MatrixClient } from "../client"; +import { MatrixClient, PendingEventOrdering } from "../client"; import { TypedReEmitter } from "../ReEmitter"; import { RelationType } from "../@types/event"; -import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "./event"; +import { EventStatus, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "./event"; import { EventTimeline } from "./event-timeline"; import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set'; import { NotificationCountType, Room, RoomEvent } from './room'; @@ -51,6 +51,7 @@ export type EventHandlerMap = { interface IThreadOpts { room: Room; client: MatrixClient; + pendingEventOrdering?: PendingEventOrdering; } export enum FeatureSupport { @@ -81,6 +82,7 @@ export class Thread extends ReadReceipt { * A reference to all the events ID at the bottom of the threads */ public readonly timelineSet: EventTimelineSet; + public timeline: MatrixEvent[] = []; private _currentUserParticipated = false; @@ -88,9 +90,12 @@ export class Thread extends ReadReceipt { private lastEvent: MatrixEvent | undefined; private replyCount = 0; + private lastPendingEvent: MatrixEvent | undefined; + private pendingReplyCount = 0; public readonly room: Room; public readonly client: MatrixClient; + private readonly pendingEventOrdering: PendingEventOrdering; public initialEventsFetched = !Thread.hasServerSideSupport; @@ -109,6 +114,7 @@ export class Thread extends ReadReceipt { this.room = opts.room; this.client = opts.client; + this.pendingEventOrdering = opts.pendingEventOrdering ?? PendingEventOrdering.Chronological; this.timelineSet = new EventTimelineSet(this.room, { timelineSupport: true, pendingEvents: true, @@ -123,11 +129,11 @@ export class Thread extends ReadReceipt { this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); this.room.on(RoomEvent.Redaction, this.onRedaction); this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho); - this.timelineSet.on(RoomEvent.Timeline, this.onEcho); + this.timelineSet.on(RoomEvent.Timeline, this.onTimelineEvent); // even if this thread is thought to be originating from this client, we initialise it as we may be in a // gappy sync and a thread around this event may already exist. - this.initialiseThread(); + this.updateThreadMetadata(); this.setEventMetadata(this.rootEvent); } @@ -181,23 +187,35 @@ export class Thread extends ReadReceipt { private onRedaction = async (event: MatrixEvent): Promise => { if (event.threadRootId !== this.id) return; // ignore redactions for other timelines if (this.replyCount <= 0) { - for (const threadEvent of this.events) { + for (const threadEvent of this.timeline) { this.clearEventMetadata(threadEvent); } this.lastEvent = this.rootEvent; this._currentUserParticipated = false; this.emit(ThreadEvent.Delete, this); } else { - await this.initialiseThread(); + await this.updateThreadMetadata(); } }; + private onTimelineEvent = ( + event: MatrixEvent, + room: Room | undefined, + toStartOfTimeline: boolean | undefined, + ): void => { + // Add a synthesized receipt when paginating forward in the timeline + if (!toStartOfTimeline) { + room!.addLocalEchoReceipt(event.getSender()!, event, ReceiptType.Read); + } + this.onEcho(event); + }; + private onEcho = async (event: MatrixEvent): Promise => { if (event.threadRootId !== this.id) return; // ignore echoes for other timelines if (this.lastEvent === event) return; if (!event.isRelation(THREAD_RELATION_TYPE.name)) return; - await this.initialiseThread(); + await this.updateThreadMetadata(); this.emit(ThreadEvent.NewReply, this, event); }; @@ -216,22 +234,23 @@ export class Thread extends ReadReceipt { roomState: this.roomState, }, ); + this.timeline = this.events; } } public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void { events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false)); - this.initialiseThread(); + this.updateThreadMetadata(); } /** * Add an event to the thread and updates * the tail/root references if needed * Will fire "Thread.update" - * @param event The event to add - * @param {boolean} toStartOfTimeline whether the event is being added + * @param event - The event to add + * @param toStartOfTimeline - whether the event is being added * to the start (and not the end) of the timeline. - * @param {boolean} emit whether to emit the Update event if the thread was updated or not. + * @param emit - whether to emit the Update event if the thread was updated or not. */ public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise { this.setEventMetadata(event); @@ -266,7 +285,7 @@ export class Thread extends ReadReceipt { if (emit) { this.emit(ThreadEvent.NewReply, this, event); - this.initialiseThread(); + this.updateThreadMetadata(); } } @@ -275,28 +294,53 @@ export class Thread extends ReadReceipt { this.setEventMetadata(event); await this.fetchEditsWhereNeeded(event); } + this.timeline = this.events; } private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship | undefined { return rootEvent?.getServerAggregatedRelation(THREAD_RELATION_TYPE.name); } - public async initialiseThread(): Promise { - let bundledRelationship = this.getRootEventBundledRelationship(); - if (Thread.hasServerSideSupport) { - await this.fetchRootEvent(); - bundledRelationship = this.getRootEventBundledRelationship(); - } - + private async processRootEvent(): Promise { + const bundledRelationship = this.getRootEventBundledRelationship(); if (Thread.hasServerSideSupport && bundledRelationship) { this.replyCount = bundledRelationship.count; this._currentUserParticipated = !!bundledRelationship.current_user_participated; const mapper = this.client.getEventMapper(); - this.lastEvent = mapper(bundledRelationship.latest_event); + // re-insert roomId + this.lastEvent = mapper({ + ...bundledRelationship.latest_event, + room_id: this.roomId, + }); await this.processEvent(this.lastEvent); } + let pendingEvents: MatrixEvent[]; + if (this.pendingEventOrdering === PendingEventOrdering.Detached) { + pendingEvents = this.room.getPendingEvents() + .filter(ev => ev.isRelation(THREAD_RELATION_TYPE.name) && this.id === ev.threadRootId); + await Promise.all(pendingEvents.map(ev => this.processEvent(ev))); + } else { + pendingEvents = this.events + .filter(ev => ev.isRelation(THREAD_RELATION_TYPE.name)) + .filter(ev => ev.status !== EventStatus.SENT && ev.status !== EventStatus.CANCELLED); + } + this.lastPendingEvent = pendingEvents.length ? pendingEvents[pendingEvents.length - 1] : undefined; + this.pendingReplyCount = pendingEvents.length; + } + + private async updateThreadMetadata(): Promise { + if (Thread.hasServerSideSupport) { + // Ensure we show *something* as soon as possible, we'll update it as soon as we get better data, but we + // don't want the thread preview to be empty if we can avoid it + if (!this.initialEventsFetched) { + await this.processRootEvent(); + } + await this.fetchRootEvent(); + } + await this.processRootEvent(); + if (!this.initialEventsFetched) { this.initialEventsFetched = true; // fetch initial event to allow proper pagination @@ -362,8 +406,8 @@ export class Thread extends ReadReceipt { * Return last reply to the thread, if known. */ public lastReply(matches: (ev: MatrixEvent) => boolean = (): boolean => true): MatrixEvent | null { - for (let i = this.events.length - 1; i >= 0; i--) { - const event = this.events[i]; + for (let i = this.timeline.length - 1; i >= 0; i--) { + const event = this.timeline[i]; if (matches(event)) { return event; } @@ -381,14 +425,14 @@ export class Thread extends ReadReceipt { * exclude annotations from that number */ public get length(): number { - return this.replyCount; + return this.replyCount + this.pendingReplyCount; } /** * A getter for the last event added to the thread, if known. */ public get replyToEvent(): Optional { - return this.lastEvent ?? this.lastReply(); + return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply(); } public get events(): MatrixEvent[] { @@ -411,10 +455,6 @@ export class Thread extends ReadReceipt { return this.timelineSet; } - public get timeline(): MatrixEvent[] { - return this.events; - } - public addReceipt(event: MatrixEvent, synthetic: boolean): void { throw new Error("Unsupported function on the thread model"); } diff --git a/src/models/typed-event-emitter.ts b/src/models/typed-event-emitter.ts index 691ec5ec3..f91500778 100644 --- a/src/models/typed-event-emitter.ts +++ b/src/models/typed-event-emitter.ts @@ -69,7 +69,7 @@ export class TypedEventEmitter< return super.listenerCount(event); } - public listeners(event: Events | EventEmitterEvents): Function[] { + public listeners(event: Events | EventEmitterEvents): ReturnType { return super.listeners(event); } @@ -119,7 +119,7 @@ export class TypedEventEmitter< return super.removeListener(event, listener); } - public rawListeners(event: Events | EventEmitterEvents): Function[] { + public rawListeners(event: Events | EventEmitterEvents): ReturnType { return super.rawListeners(event); } } diff --git a/src/models/user.ts b/src/models/user.ts index 5d92ca494..3dd199f3b 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module models/user - */ - import { MatrixEvent } from "./event"; import { TypedEventEmitter } from "./typed-event-emitter"; @@ -30,51 +26,132 @@ export enum UserEvent { } export type UserEventHandlerMap = { + /** + * Fires whenever any user's display name changes. + * @param event - The matrix event which caused this event to fire. + * @param user - The user whose User.displayName changed. + * @example + * ``` + * matrixClient.on("User.displayName", function(event, user){ + * var newName = user.displayName; + * }); + * ``` + */ [UserEvent.DisplayName]: (event: MatrixEvent | undefined, user: User) => void; + /** + * Fires whenever any user's avatar URL changes. + * @param event - The matrix event which caused this event to fire. + * @param user - The user whose User.avatarUrl changed. + * @example + * ``` + * matrixClient.on("User.avatarUrl", function(event, user){ + * var newUrl = user.avatarUrl; + * }); + * ``` + */ [UserEvent.AvatarUrl]: (event: MatrixEvent | undefined, user: User) => void; + /** + * Fires whenever any user's presence changes. + * @param event - The matrix event which caused this event to fire. + * @param user - The user whose User.presence changed. + * @example + * ``` + * matrixClient.on("User.presence", function(event, user){ + * var newPresence = user.presence; + * }); + * ``` + */ [UserEvent.Presence]: (event: MatrixEvent | undefined, user: User) => void; + /** + * Fires whenever any user's currentlyActive changes. + * @param event - The matrix event which caused this event to fire. + * @param user - The user whose User.currentlyActive changed. + * @example + * ``` + * matrixClient.on("User.currentlyActive", function(event, user){ + * var newCurrentlyActive = user.currentlyActive; + * }); + * ``` + */ [UserEvent.CurrentlyActive]: (event: MatrixEvent | undefined, user: User) => void; + /** + * Fires whenever any user's lastPresenceTs changes, + * ie. whenever any presence event is received for a user. + * @param event - The matrix event which caused this event to fire. + * @param user - The user whose User.lastPresenceTs changed. + * @example + * ``` + * matrixClient.on("User.lastPresenceTs", function(event, user){ + * var newlastPresenceTs = user.lastPresenceTs; + * }); + * ``` + */ [UserEvent.LastPresenceTs]: (event: MatrixEvent | undefined, user: User) => void; }; export class User extends TypedEventEmitter { private modified = -1; - // XXX these should be read-only + /** + * The 'displayname' of the user if known. + * @privateRemarks + * Should be read-only + */ public displayName?: string; public rawDisplayName?: string; + /** + * The 'avatar_url' of the user if known. + * @privateRemarks + * Should be read-only + */ public avatarUrl?: string; + /** + * The presence status message if known. + * @privateRemarks + * Should be read-only + */ public presenceStatusMsg?: string; + /** + * The presence enum if known. + * @privateRemarks + * Should be read-only + */ public presence = "offline"; + /** + * Timestamp (ms since the epoch) for when we last received presence data for this user. + * We can subtract lastActiveAgo from this to approximate an absolute value for when a user was last active. + * @privateRemarks + * Should be read-only + */ public lastActiveAgo = 0; + /** + * The time elapsed in ms since the user interacted proactively with the server, + * or we saw a message from the user + * @privateRemarks + * Should be read-only + */ public lastPresenceTs = 0; + /** + * Whether we should consider lastActiveAgo to be an approximation + * and that the user should be seen as active 'now' + * @privateRemarks + * Should be read-only + */ public currentlyActive = false; + /** + * The events describing this user. + * @privateRemarks + * Should be read-only + */ public events: { + /** The m.presence event for this user. */ presence?: MatrixEvent; profile?: MatrixEvent; } = {}; /** - * Construct a new User. A User must have an ID and can optionally have extra - * information associated with it. - * @constructor - * @param {string} userId Required. The ID of this user. - * @prop {string} userId The ID of the user. - * @prop {Object} info The info object supplied in the constructor. - * @prop {string} displayName The 'displayname' of the user if known. - * @prop {string} avatarUrl The 'avatar_url' of the user if known. - * @prop {string} presence The presence enum if known. - * @prop {string} presenceStatusMsg The presence status message if known. - * @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted - * proactively with the server, or we saw a message from the user - * @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last - * received presence data for this user. We can subtract - * lastActiveAgo from this to approximate an absolute value for - * when a user was last active. - * @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be - * an approximation and that the user should be seen as active 'now' - * @prop {Object} events The events describing this user. - * @prop {MatrixEvent} events.presence The m.presence event for this user. + * Construct a new User. A User must have an ID and can optionally have extra information associated with it. + * @param userId - Required. The ID of this user. */ public constructor(public readonly userId: string) { super(); @@ -87,10 +164,12 @@ export class User extends TypedEventEmitter { * Update this User with the given presence event. May fire "User.presence", * "User.avatarUrl" and/or "User.displayName" if this event updates this user's * properties. - * @param {MatrixEvent} event The m.presence event. - * @fires module:client~MatrixClient#event:"User.presence" - * @fires module:client~MatrixClient#event:"User.displayName" - * @fires module:client~MatrixClient#event:"User.avatarUrl" + * @param event - The `m.presence` event. + * + * @remarks + * Fires {@link UserEvent.Presence} + * Fires {@link UserEvent.DisplayName} + * Fires {@link UserEvent.AvatarUrl} */ public setPresenceEvent(event: MatrixEvent): void { if (event.getType() !== "m.presence") { @@ -142,7 +221,7 @@ export class User extends TypedEventEmitter { /** * Manually set this user's display name. No event is emitted in response to this * as there is no underlying MatrixEvent to emit with. - * @param {string} name The new display name. + * @param name - The new display name. */ public setDisplayName(name: string): void { const oldName = this.displayName; @@ -155,7 +234,7 @@ export class User extends TypedEventEmitter { /** * Manually set this user's non-disambiguated display name. No event is emitted * in response to this as there is no underlying MatrixEvent to emit with. - * @param {string} name The new display name. + * @param name - The new display name. */ public setRawDisplayName(name?: string): void { this.rawDisplayName = name; @@ -164,7 +243,7 @@ export class User extends TypedEventEmitter { /** * Manually set this user's avatar URL. No event is emitted in response to this * as there is no underlying MatrixEvent to emit with. - * @param {string} url The new avatar URL. + * @param url - The new avatar URL. */ public setAvatarUrl(url?: string): void { const oldUrl = this.avatarUrl; @@ -185,7 +264,7 @@ export class User extends TypedEventEmitter { * Get the timestamp when this User was last updated. This timestamp is * updated when this User receives a new Presence event which has updated a * property on this object. It is updated before firing events. - * @return {number} The timestamp + * @returns The timestamp */ public getLastModifiedTime(): number { return this.modified; @@ -194,65 +273,9 @@ export class User extends TypedEventEmitter { /** * Get the absolute timestamp when this User was last known active on the server. * It is *NOT* accurate if this.currentlyActive is true. - * @return {number} The timestamp + * @returns The timestamp */ public getLastActiveTs(): number { return this.lastPresenceTs - this.lastActiveAgo; } } - -/** - * Fires whenever any user's lastPresenceTs changes, - * ie. whenever any presence event is received for a user. - * @event module:client~MatrixClient#"User.lastPresenceTs" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.lastPresenceTs changed. - * @example - * matrixClient.on("User.lastPresenceTs", function(event, user){ - * var newlastPresenceTs = user.lastPresenceTs; - * }); - */ - -/** - * Fires whenever any user's presence changes. - * @event module:client~MatrixClient#"User.presence" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.presence changed. - * @example - * matrixClient.on("User.presence", function(event, user){ - * var newPresence = user.presence; - * }); - */ - -/** - * Fires whenever any user's currentlyActive changes. - * @event module:client~MatrixClient#"User.currentlyActive" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.currentlyActive changed. - * @example - * matrixClient.on("User.currentlyActive", function(event, user){ - * var newCurrentlyActive = user.currentlyActive; - * }); - */ - -/** - * Fires whenever any user's display name changes. - * @event module:client~MatrixClient#"User.displayName" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.displayName changed. - * @example - * matrixClient.on("User.displayName", function(event, user){ - * var newName = user.displayName; - * }); - */ - -/** - * Fires whenever any user's avatar URL changes. - * @event module:client~MatrixClient#"User.avatarUrl" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.avatarUrl changed. - * @example - * matrixClient.on("User.avatarUrl", function(event, user){ - * var newUrl = user.avatarUrl; - * }); - */ diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts index 4538b9721..e6a307692 100644 --- a/src/pushprocessor.ts +++ b/src/pushprocessor.ts @@ -38,10 +38,6 @@ import { } from "./@types/PushRules"; import { EventType } from "./@types/event"; -/** - * @module pushprocessor - */ - const RULEKINDS_IN_ORDER = [ PushRuleKind.Override, PushRuleKind.ContentSpecific, @@ -116,25 +112,27 @@ const DEFAULT_UNDERRIDE_RULES: IPushRule[] = [ ]; export interface IActionsObject { + /** Whether this event should notify the user or not. */ notify: boolean; + /** How this event should be notified. */ tweaks: Partial>; } export class PushProcessor { /** * Construct a Push Processor. - * @constructor - * @param {Object} client The Matrix client object to use + * @param client - The Matrix client object to use */ public constructor(private readonly client: MatrixClient) {} /** * Convert a list of actions into a object with the actions as keys and their values - * eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ] - * becomes { notify: true, tweaks: { sound: 'default' } } - * @param {array} actionList The actions list + * @example + * eg. `[ 'notify', { set_tweak: 'sound', value: 'default' } ]` + * becomes `{ notify: true, tweaks: { sound: 'default' } }` + * @param actionList - The actions list * - * @return {object} A object with key 'notify' (true or false) and an object of actions + * @returns A object with key 'notify' (true or false) and an object of actions */ public static actionListToActionsObject(actionList: PushRuleAction[]): IActionsObject { const actionObj: IActionsObject = { notify: false, tweaks: {} }; @@ -155,8 +153,8 @@ export class PushProcessor { * Rewrites conditions on a client's push rules to match the defaults * where applicable. Useful for upgrading push rules to more strict * conditions when the server is falling behind on defaults. - * @param {object} incomingRules The client's existing push rules - * @returns {object} The rewritten rules + * @param incomingRules - The client's existing push rules + * @returns The rewritten rules */ public static rewriteDefaultRules(incomingRules: IPushRules): IPushRules { let newRules: IPushRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone @@ -502,10 +500,6 @@ export class PushProcessor { /** * Get the user's push actions for the given event - * - * @param {module:models/event.MatrixEvent} ev - * - * @return {PushAction} */ public actionsForEvent(ev: MatrixEvent): IActionsObject { return this.pushActionsForEventAndRulesets(ev, this.client.pushRules); @@ -514,17 +508,17 @@ export class PushProcessor { /** * Get one of the users push rules by its ID * - * @param {string} ruleId The ID of the rule to search for - * @return {object} The push rule, or null if no such rule was found + * @param ruleId - The ID of the rule to search for + * @returns 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; } } @@ -532,15 +526,3 @@ export class PushProcessor { return null; } } - -/** - * @typedef {Object} PushAction - * @type {Object} - * @property {boolean} notify Whether this event should notify the user or not. - * @property {Object} tweaks How this event should be notified. - * @property {boolean} tweaks.highlight Whether this event should be highlighted - * on the UI. - * @property {boolean} tweaks.sound Whether this notification should produce a - * noise. - */ - diff --git a/src/realtime-callbacks.ts b/src/realtime-callbacks.ts index 4677b0c1e..005e9816b 100644 --- a/src/realtime-callbacks.ts +++ b/src/realtime-callbacks.ts @@ -48,16 +48,17 @@ type Callback = { const callbackList: Callback[] = []; // var debuglog = logger.log.bind(logger); +/* istanbul ignore next */ const debuglog = function(...params: any[]): void {}; /** * reimplementation of window.setTimeout, which will call the callback if * the wallclock time goes past the deadline. * - * @param {function} func callback to be called after a delay - * @param {Number} delayMs number of milliseconds to delay by + * @param func - callback to be called after a delay + * @param delayMs - number of milliseconds to delay by * - * @return {Number} an identifier for this callback, which may be passed into + * @returns an identifier for this callback, which may be passed into * clearTimeout later. */ export function setTimeout(func: (...params: any[]) => void, delayMs: number, ...params: any[]): number { @@ -93,7 +94,7 @@ export function setTimeout(func: (...params: any[]) => void, delayMs: number, .. /** * reimplementation of window.clearTimeout, which mirrors setTimeout * - * @param {Number} key result from an earlier setTimeout call + * @param key - result from an earlier setTimeout call */ export function clearTimeout(key: number): void { if (callbackList.length === 0) { diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index a93d4c476..d4ed1bc17 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -67,9 +67,9 @@ export class MSC3906Rendezvous { private _code?: string; /** - * @param channel The secure channel used for communication - * @param client The Matrix client in used on the device already logged in - * @param onFailure Callback for when the rendezvous fails + * @param channel - The secure channel used for communication + * @param client - The Matrix client in used on the device already logged in + * @param onFailure - Callback for when the rendezvous fails */ public constructor( private channel: RendezvousChannel, @@ -217,7 +217,7 @@ export class MSC3906Rendezvous { /** * Verify the device and cross-sign it. - * @param timeout time in milliseconds to wait for device to come online + * @param timeout - time in milliseconds to wait for device to come online * @returns the new device info if the device was verified */ public async verifyNewDeviceOnExistingDevice( diff --git a/src/rendezvous/RendezvousChannel.ts b/src/rendezvous/RendezvousChannel.ts index 9b1060304..f5c35217f 100644 --- a/src/rendezvous/RendezvousChannel.ts +++ b/src/rendezvous/RendezvousChannel.ts @@ -28,7 +28,7 @@ export interface RendezvousChannel { /** * Send a payload via the channel. - * @param data payload to send + * @param data - payload to send */ send(data: T): Promise; diff --git a/src/rendezvous/RendezvousTransport.ts b/src/rendezvous/RendezvousTransport.ts index 231a0f1c4..8721febff 100644 --- a/src/rendezvous/RendezvousTransport.ts +++ b/src/rendezvous/RendezvousTransport.ts @@ -41,7 +41,7 @@ export interface RendezvousTransport { /** * Send data via the transport. - * @param data the data itself + * @param data - the data itself */ send(data: T): Promise; @@ -52,7 +52,7 @@ export interface RendezvousTransport { /** * Cancel the rendezvous. This will call `onCancelled()` if it is set. - * @param reason the reason for the cancellation/failure + * @param reason - the reason for the cancellation/failure */ cancel(reason: RendezvousFailureReason): Promise; } diff --git a/src/room-hierarchy.ts b/src/room-hierarchy.ts index c15f2ac56..3ed020527 100644 --- a/src/room-hierarchy.ts +++ b/src/room-hierarchy.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module room-hierarchy - */ - import { Room } from "./models/room"; import { IHierarchyRoom, IHierarchyRelation } from "./@types/spaces"; import { MatrixClient } from "./client"; @@ -41,11 +37,10 @@ export class RoomHierarchy { * * A RoomHierarchy instance allows you to easily make use of the /hierarchy API and paginate it. * - * @param {Room} root the root of this hierarchy - * @param {number} pageSize the maximum number of rooms to return per page, can be overridden per load request. - * @param {number} maxDepth the maximum depth to traverse the hierarchy to - * @param {boolean} suggestedOnly whether to only return rooms with suggested=true. - * @constructor + * @param root - the root of this hierarchy + * @param pageSize - the maximum number of rooms to return per page, can be overridden per load request. + * @param maxDepth - the maximum depth to traverse the hierarchy to + * @param suggestedOnly - whether to only return rooms with suggested=true. */ public constructor( public readonly root: Room, diff --git a/src/scheduler.ts b/src/scheduler.ts index 0c15032bc..d6cfef362 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -17,7 +17,6 @@ limitations under the License. /** * This is an internal module which manages queuing, scheduling and retrying * of requests. - * @module scheduler */ import * as utils from "./utils"; import { logger } from './logger'; @@ -35,20 +34,13 @@ interface IQueueEntry { attempts: number; } +/** + * The function to invoke to process (send) events in the queue. + * @param event - The event to send. + * @returns Resolved/rejected depending on the outcome of the request. + */ type ProcessFunction = (event: MatrixEvent) => Promise; -/** - * Construct a scheduler for Matrix. Requires - * {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided - * with a way of processing events. - * @constructor - * @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry - * algorithm to apply when determining when to try to send an event again. - * Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}. - * @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing - * algorithm to apply when determining which events should be sent before the - * given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}. - */ // eslint-disable-next-line camelcase export class MatrixScheduler { /** @@ -56,11 +48,8 @@ export class MatrixScheduler { * times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the * failure was due to a rate limited request, the time specified in the error is * waited before being retried. - * @param {MatrixEvent} event - * @param {Number} attempts Number of attempts that have been made, including the one that just failed (ie. starting at 1) - * @param {MatrixError} err - * @return {Number} - * @see module:scheduler~retryAlgorithm + * @param attempts - Number of attempts that have been made, including the one that just failed (ie. starting at 1) + * @see retryAlgorithm */ // eslint-disable-next-line @typescript-eslint/naming-convention public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent | null, attempts: number, err: MatrixError): number { @@ -90,14 +79,12 @@ export class MatrixScheduler { } /** - * Queues m.room.message events and lets other events continue + * Queues `m.room.message` events and lets other events continue * concurrently. - * @param {MatrixEvent} event - * @return {string} - * @see module:scheduler~queueAlgorithm + * @see 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. @@ -116,16 +103,52 @@ export class MatrixScheduler { private activeQueues: string[] = []; private procFn: ProcessFunction | null = null; + /** + * Construct a scheduler for Matrix. Requires + * {@link MatrixScheduler#setProcessFunction} to be provided + * with a way of processing events. + * @param retryAlgorithm - Optional. The retry + * algorithm to apply when determining when to try to send an event again. + * Defaults to {@link MatrixScheduler.RETRY_BACKOFF_RATELIMIT}. + * @param queueAlgorithm - Optional. The queuing + * algorithm to apply when determining which events should be sent before the + * given event. Defaults to {@link MatrixScheduler.QUEUE_MESSAGES}. + */ public constructor( + /** + * The retry algorithm to apply when retrying events. To stop retrying, return + * `-1`. If this event was part of a queue, it will be removed from + * the queue. + * @param event - The event being retried. + * @param attempts - The number of failed attempts. This will always be \>= 1. + * @param err - The most recent error message received when trying + * to send this event. + * @returns The number of milliseconds to wait before trying again. If + * this is 0, the request will be immediately retried. If this is + * `-1`, the event will be marked as + * {@link EventStatus.NOT_SENT} and will not be retried. + */ public readonly retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT, + /** + * The queuing algorithm to apply to events. This function must be idempotent as + * it may be called multiple times with the same event. All queues created are + * serviced in a FIFO manner. To send the event ASAP, return `null` + * which will not put this event in a queue. Events that fail to send that form + * part of a queue will be removed from the queue and the next event in the + * queue will be sent. + * @param event - The event to be sent. + * @returns The name of the queue to put the event into. If a queue with + * this name does not exist, it will be created. If this is `null`, + * the event is not put into a queue and will be sent concurrently. + */ public readonly queueAlgorithm = MatrixScheduler.QUEUE_MESSAGES, ) {} /** * Retrieve a queue based on an event. The event provided does not need to be in * the queue. - * @param {MatrixEvent} event An event to get the queue for. - * @return {?Array} A shallow copy of events in the queue or null. + * @param event - An event to get the queue for. + * @returns A shallow copy of events in the queue or null. * Modifying this array will not modify the list itself. Modifying events in * this array will modify the underlying event in the queue. * @see MatrixScheduler.removeEventFromQueue To remove an event from the queue. @@ -143,8 +166,8 @@ export class MatrixScheduler { /** * Remove this event from the queue. The event is equal to another event if they * have the same ID returned from event.getId(). - * @param {MatrixEvent} event The event to remove. - * @return {boolean} True if this event was removed. + * @param event - The event to remove. + * @returns True if this event was removed. */ public removeEventFromQueue(event: MatrixEvent): boolean { const name = this.queueAlgorithm(event); @@ -168,7 +191,7 @@ export class MatrixScheduler { * Set the process function. Required for events in the queue to be processed. * If set after events have been added to the queue, this will immediately start * processing them. - * @param {module:scheduler~processFn} fn The function that can process events + * @param fn - The function that can process events * in the queue. */ public setProcessFunction(fn: ProcessFunction): void { @@ -178,8 +201,8 @@ export class MatrixScheduler { /** * Queue an event if it is required and start processing queues. - * @param {MatrixEvent} event The event that may be queued. - * @return {?Promise} A promise if the event was queued, which will be + * @param event - The event that may be queued. + * @returns A promise if the event was queued, which will be * resolved or rejected in due time, else null. */ public queueEvent(event: MatrixEvent): Promise | null { @@ -283,46 +306,9 @@ export class MatrixScheduler { } } +/* istanbul ignore next */ function debuglog(...args: any[]): void { if (DEBUG) { logger.log(...args); } } - -/** - * The retry algorithm to apply when retrying events. To stop retrying, return - * -1. If this event was part of a queue, it will be removed from - * the queue. - * @callback retryAlgorithm - * @param {MatrixEvent} event The event being retried. - * @param {Number} attempts The number of failed attempts. This will always be - * >= 1. - * @param {MatrixError} err The most recent error message received when trying - * to send this event. - * @return {Number} The number of milliseconds to wait before trying again. If - * this is 0, the request will be immediately retried. If this is - * -1, the event will be marked as - * {@link module:models/event.EventStatus.NOT_SENT} and will not be retried. - */ - -/** - * The queuing algorithm to apply to events. This function must be idempotent as - * it may be called multiple times with the same event. All queues created are - * serviced in a FIFO manner. To send the event ASAP, return null - * which will not put this event in a queue. Events that fail to send that form - * part of a queue will be removed from the queue and the next event in the - * queue will be sent. - * @callback queueAlgorithm - * @param {MatrixEvent} event The event to be sent. - * @return {string} The name of the queue to put the event into. If a queue with - * this name does not exist, it will be created. If this is null, - * the event is not put into a queue and will be sent concurrently. - */ - -/** - * The function to invoke to process (send) events in the queue. - * @callback processFn - * @param {MatrixEvent} event The event to send. - * @return {Promise} Resolved/rejected depending on the outcome of the request. - */ - diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index a787b4e34..f9bddd875 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), @@ -414,7 +458,7 @@ export class SlidingSyncSdk { /** * Sync rooms the user has left. - * @return {Promise} Resolved when they've been added to the store. + * @returns Resolved when they've been added to the store. */ public async syncLeftRooms(): Promise { return []; // TODO @@ -423,8 +467,8 @@ export class SlidingSyncSdk { /** * Peek into a room. This will result in the room in question being synced so it * is accessible via getRooms(). Live updates for the room will be provided. - * @param {string} roomId The room ID to peek into. - * @return {Promise} A promise which resolves once the room has been added to the + * @param roomId - The room ID to peek into. + * @returns A promise which resolves once the room has been added to the * store. */ public async peek(_roomId: string): Promise { @@ -441,8 +485,7 @@ export class SlidingSyncSdk { /** * Returns the current state of this sync object - * @see module:client~MatrixClient#event:"sync" - * @return {?String} + * @see MatrixClient#event:"sync" */ public getSyncState(): SyncState | null { return this.syncState; @@ -454,7 +497,6 @@ export class SlidingSyncSdk { * such data. * Sync errors, if available, are put in the 'error' key of * this object. - * @return {?Object} */ public getSyncStateData(): ISyncStateData | null { return this.syncStateData ?? null; @@ -533,7 +575,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 @@ -681,7 +723,7 @@ export class SlidingSyncSdk { } } */ - this.injectRoomEvents(room, stateEvents, timelineEvents, false); + this.injectRoomEvents(room, stateEvents, timelineEvents, roomData.num_live); // we deliberately don't add ephemeral events to the timeline room.addEphemeralEvents(ephemeralEvents); @@ -703,7 +745,7 @@ export class SlidingSyncSdk { const processRoomEvent = async (e: MatrixEvent): Promise => { client.emit(ClientEvent.Event, e); if (e.isState() && e.getType() == EventType.RoomEncryption && this.opts.crypto) { - await this.opts.crypto.onCryptoEvent(e); + await this.opts.crypto.onCryptoEvent(room, e); } }; @@ -721,21 +763,22 @@ export class SlidingSyncSdk { /** * Injects events into a room's model. - * @param {Room} room - * @param {MatrixEvent[]} stateEventList A list of state events. This is the state + * @param stateEventList - A list of state events. This is the state * at the *START* of the timeline list if it is supplied. - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index - * @param {boolean} fromCache whether the sync response came from cache + * @param timelineEventList - A list of timeline events. Lower index * is earlier in time. Higher index is later. + * @param numLive - the number of events in timelineEventList which just happened, + * supplied from the server. */ public injectRoomEvents( room: Room, stateEventList: MatrixEvent[], timelineEventList?: MatrixEvent[], - fromCache = false, + numLive?: number, ): void { timelineEventList = timelineEventList || []; stateEventList = stateEventList || []; + numLive = numLive || 0; // If there are no events in the timeline yet, initialise it with // the given state events @@ -774,13 +817,31 @@ export class SlidingSyncSdk { room.currentState.setStateEvents(stateEventList); } + // the timeline is broken into 'live' events which just happened and normal timeline events + // which are still to be appended to the end of the live timeline but happened a while ago. + // The live events are marked as fromCache=false to ensure that downstream components know + // this is a live event, not historical (from a remote server cache). + + let liveTimelineEvents: MatrixEvent[] = []; + if (numLive > 0) { + // last numLive events are live + liveTimelineEvents = timelineEventList.slice(-1 * numLive); + // everything else is not live + timelineEventList = timelineEventList.slice(0, -1 * liveTimelineEvents.length); + } + // execute the timeline events. This will continue to diverge the current state // if the timeline has any state events in it. // This also needs to be done before running push rules on the events as they need // to be decorated with sender etc. room.addLiveEvents(timelineEventList, { - fromCache: fromCache, + fromCache: true, }); + if (liveTimelineEvents.length > 0) { + room.addLiveEvents(liveTimelineEvents, { + fromCache: false, + }); + } room.recalculate(); @@ -869,8 +930,8 @@ export class SlidingSyncSdk { /** * Sets the sync state and emits an event to say so - * @param {String} newState The new state string - * @param {Object} data Object of additional data to emit in the event + * @param newState - The new state string + * @param data - Object of additional data to emit in the event */ private updateSyncState(newState: SyncState, data?: ISyncStateData): void { const old = this.syncState; @@ -884,7 +945,7 @@ export class SlidingSyncSdk { * as appropriate. * This must be called after the room the events belong to has been stored. * - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index + * @param timelineEventList - A list of timeline events. Lower index * is earlier in time. Higher index is later. */ private addNotifications(timelineEventList: MatrixEvent[]): void { @@ -947,12 +1008,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 171bf55c3..81a67f05e 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -95,6 +95,7 @@ export interface MSC3575RoomData { limited?: boolean; is_dm?: boolean; prev_batch?: string; + num_live?: number; } interface ListResponse { @@ -138,7 +139,7 @@ export interface MSC3575SlidingSyncResponse { txn_id?: string; lists: ListResponse[]; rooms: Record; - extensions: object; + extensions: Record; } export enum SlidingSyncState { @@ -155,7 +156,7 @@ export enum SlidingSyncState { /** * Internal Class. SlidingList represents a single list in sliding sync. The list can have filters, - * multiple sliding windows, and maintains the index->room_id mapping. + * multiple sliding windows, and maintains the index-\>room_id mapping. */ class SlidingList { private list!: MSC3575List; @@ -167,7 +168,7 @@ class SlidingList { /** * Construct a new sliding list. - * @param {MSC3575List} list The range, sort and filter values to use for this list. + * @param list - The range, sort and filter values to use for this list. */ public constructor(list: MSC3575List) { this.replaceList(list); @@ -176,7 +177,7 @@ class SlidingList { /** * Mark this list as modified or not. Modified lists will return sticky params with calls to getList. * This is useful for the first time the list is sent, or if the list has changed in some way. - * @param modified True to mark this list as modified so all sticky parameters will be re-sent. + * @param modified - True to mark this list as modified so all sticky parameters will be re-sent. */ public setModified(modified: boolean): void { this.isModified = modified; @@ -184,7 +185,7 @@ class SlidingList { /** * Update the list range for this list. Does not affect modified status as list ranges are non-sticky. - * @param newRanges The new ranges for the list + * @param newRanges - The new ranges for the list */ public updateListRange(newRanges: number[][]): void { this.list.ranges = JSON.parse(JSON.stringify(newRanges)); @@ -192,7 +193,7 @@ class SlidingList { /** * Replace list parameters. All fields will be replaced with the new list parameters. - * @param list The new list parameters + * @param list - The new list parameters */ public replaceList(list: MSC3575List): void { list.filters = list.filters || {}; @@ -212,7 +213,7 @@ class SlidingList { /** * Return a copy of the list suitable for a request body. - * @param {boolean} forceIncludeAllParams True to forcibly include all params even if the list + * @param forceIncludeAllParams - True to forcibly include all params even if the list * hasn't been modified. Callers may want to do this if they are modifying the list prior to calling * updateList. */ @@ -234,7 +235,7 @@ class SlidingList { * a b c d _ f COMMAND: DELETE 7; * e a b c d f COMMAND: INSERT 0 e; * c=3 is wrong as we are not tracking it, ergo we need to see if `i` is in range else drop it - * @param i The index to check + * @param i - The index to check * @returns True if the index is within a sliding window */ public isIndexInRange(i: number): boolean { @@ -264,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. @@ -273,15 +274,15 @@ export interface Extension { /** * A function which is called when the request JSON is being formed. * Returns the data to insert under this key. - * @param isInitial True when this is part of the initial request (send sticky params) + * @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. + * @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. @@ -352,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(); @@ -367,11 +368,11 @@ export class SlidingSync extends TypedEventEmitter} | null { @@ -434,7 +435,7 @@ 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]); @@ -541,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]); @@ -551,8 +552,8 @@ export class SlidingSync extends TypedEventEmitter void) => void; - /** @return {Promise} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ isNewlyCreated(): Promise; /** * Get the sync token. - * @return {string} */ getSyncToken(): string | null; /** * Set the sync token. - * @param {string} token */ setSyncToken(token: string): void; /** * Store the given room. - * @param {Room} room The room to be stored. All properties must be stored. + * @param room - The room to be stored. All properties must be stored. */ storeRoom(room: Room): void; /** * Retrieve a room by its' room ID. - * @param {string} roomId The room ID. - * @return {Room} The room or null. + * @param roomId - The room ID. + * @returns The room or null. */ getRoom(roomId: string): Room | null; /** * Retrieve all known rooms. - * @return {Room[]} A list of rooms, which may be empty. + * @returns A list of rooms, which may be empty. */ getRooms(): Room[]; /** * Permanently delete a room. - * @param {string} roomId */ removeRoom(roomId: string): void; /** * Retrieve a summary of all the rooms. - * @return {RoomSummary[]} A summary of each room. + * @returns A summary of each room. */ getRoomSummaries(): RoomSummary[]; /** * Store a User. - * @param {User} user The user to store. + * @param user - The user to store. */ storeUser(user: User): void; /** * Retrieve a User by its' user ID. - * @param {string} userId The user ID. - * @return {User} The user or null. + * @param userId - The user ID. + * @returns The user or null. */ getUser(userId: string): User | null; /** * Retrieve all known users. - * @return {User[]} A list of users, which may be empty. + * @returns A list of users, which may be empty. */ getUsers(): User[]; /** * Retrieve scrollback for this room. - * @param {Room} room The matrix room - * @param {number} limit The max number of old events to retrieve. - * @return {Array} An array of objects which will be at most 'limit' + * @param room - The matrix room + * @param limit - The max number of old events to retrieve. + * @returns An array of objects which will be at most 'limit' * length and at least 0. The objects are the raw event JSON. */ scrollback(room: Room, limit: number): MatrixEvent[]; /** * Store events for a room. - * @param {Room} room The room to store events for. - * @param {Array} events The events to store. - * @param {string} token The token associated with these events. - * @param {boolean} toStart True if these are paginated results. + * @param room - The room to store events for. + * @param events - The events to store. + * @param token - The token associated with these events. + * @param 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. - * @param {Filter} filter */ storeFilter(filter: Filter): void; /** * Retrieve a filter. - * @param {string} userId - * @param {string} filterId - * @return {?Filter} A filter or null. + * @returns A filter or null. */ getFilter(userId: string, filterId: string): Filter | null; /** * Retrieve a filter ID with the given name. - * @param {string} filterName The filter name. - * @return {?string} The filter ID or null. + * @param filterName - The filter name. + * @returns The filter ID or null. */ getFilterIdByName(filterName: string): string | null; /** * Set a filter name to ID mapping. - * @param {string} filterName - * @param {string} filterId */ setFilterIdByName(filterName: string, filterId?: string): void; /** * Store user-scoped account data events - * @param {Array} events The events to store. + * @param events - The events to store. */ storeAccountDataEvents(events: MatrixEvent[]): void; /** * Get account data event by event type - * @param {string} eventType The event type being queried + * @param eventType - The event type being queried */ getAccountData(eventType: EventType | string): MatrixEvent | undefined; /** * setSyncData does nothing as there is no backing data store. * - * @param {Object} syncData The sync data - * @return {Promise} An immediately resolved promise. + * @param syncData - The sync data + * @returns An immediately resolved promise. */ setSyncData(syncData: ISyncResponse): Promise; /** * We never want to save because we have nothing to save to. * - * @return {boolean} If the store wants to save + * @returns If the store wants to save */ wantsSave(): boolean; @@ -187,19 +179,19 @@ export interface IStore { /** * Startup does nothing. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ startup(): Promise; /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ getSavedSync(): Promise; /** - * @return {Promise} If there is a saved sync, the nextBatch token + * @returns If there is a saved sync, the nextBatch token * for this sync, otherwise null. */ getSavedSyncToken(): Promise; @@ -207,16 +199,15 @@ export interface IStore { /** * Delete all data from this store. Does nothing since this store * doesn't store anything. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ deleteAllData(): Promise; /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ getOutOfBandMembers(roomId: string): Promise; @@ -224,9 +215,8 @@ export interface IStore { * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store - * @returns {Promise} when all members have been stored + * @param membershipEvents - the membership events to store + * @returns when all members have been stored */ setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise; diff --git a/src/store/indexeddb-local-backend.ts b/src/store/indexeddb-local-backend.ts index af4b5fc65..fe61f11df 100644 --- a/src/store/indexeddb-local-backend.ts +++ b/src/store/indexeddb-local-backend.ts @@ -51,12 +51,12 @@ const VERSION = DB_MIGRATIONS.length; /** * Helper method to collect results from a Cursor and promiseify it. - * @param {ObjectStore|Index} store The store to perform openCursor on. - * @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor. - * @param {Function} resultMapper A function which is repeatedly called with a + * @param store - The store to perform openCursor on. + * @param keyRange - Optional key range to apply on the cursor. + * @param resultMapper - A function which is repeatedly called with a * Cursor. * Return the data you want to keep. - * @return {Promise} Resolves to an array of whatever you returned from + * @returns Promise which resolves to an array of whatever you returned from * resultMapper. */ function selectQuery( @@ -134,11 +134,10 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * Does the actual reading from and writing to the indexeddb * * Construct a new Indexed Database store backend. This requires a call to - * connect() before this store can be used. - * @constructor - * @param {Object} indexedDB The Indexed DB interface e.g - * window.indexedDB - * @param {string=} dbName Optional database name. The same name must be used + * `connect()` before this store can be used. + * @param indexedDB - The Indexed DB interface e.g + * `window.indexedDB` + * @param dbName - Optional database name. The same name must be used * to open the same database. */ public constructor(private readonly indexedDB: IDBFactory, dbName = "default") { @@ -149,7 +148,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Attempt to connect to the database. This can fail if the user does not * grant permission. - * @return {Promise} Resolves if successfully connected. + * @returns Promise which resolves if successfully connected. */ public connect(): Promise { if (!this.disconnected) { @@ -195,14 +194,14 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { }); } - /** @return {boolean} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ public isNewlyCreated(): Promise { return Promise.resolve(this._isNewlyCreated); } /** * Having connected, load initial data from the database and prepare for use - * @return {Promise} Resolves on success + * @returns Promise which resolves on success */ private init(): Promise { return Promise.all([ @@ -223,9 +222,8 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {Promise} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ public getOutOfBandMembers(roomId: string): Promise { return new Promise((resolve, reject) => { @@ -273,8 +271,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store + * @param membershipEvents - the membership events to store */ public async setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise { logger.log(`LL: backend about to store ${membershipEvents.length}` + @@ -315,10 +312,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]); @@ -339,7 +336,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Clear the entire database. This should be used when logging out of a client * to prevent mixing data between accounts. - * @return {Promise} Resolved when the database is cleared. + * @returns Resolved when the database is cleared. */ public clearDatabase(): Promise { return new Promise((resolve) => { @@ -366,11 +363,11 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { } /** - * @param {boolean=} copy If false, the data returned is from internal + * @param copy - If false, the data returned is from internal * buffers and must not be mutated. Otherwise, a copy is made before * returning such that the data can be safely mutated. Default: true. * - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ @@ -421,9 +418,9 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Persist rooms /sync data along with the next batch token. - * @param {string} nextBatch The next_batch /sync value. - * @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator - * @return {Promise} Resolves if the data was persisted. + * @param nextBatch - The next_batch /sync value. + * @param roomsData - The 'rooms' /sync data from a SyncAccumulator + * @returns Promise which resolves if the data was persisted. */ private persistSyncData( nextBatch: string, @@ -447,8 +444,8 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Persist a list of account data events. Events with the same 'type' will * be replaced. - * @param {Object[]} accountData An array of raw user-scoped account data events - * @return {Promise} Resolves if the events were persisted. + * @param accountData - An array of raw user-scoped account data events + * @returns Promise which resolves if the events were persisted. */ private persistAccountData(accountData: IMinimalEvent[]): Promise { return utils.promiseTry(() => { @@ -466,8 +463,8 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * Users with the same 'userId' will be replaced. * Presence events should be the event in its raw form (not the Event * object) - * @param {Object[]} tuples An array of [userid, event] tuples - * @return {Promise} Resolves if the users were persisted. + * @param tuples - An array of [userid, event] tuples + * @returns Promise which resolves if the users were persisted. */ private persistUserPresenceEvents(tuples: UserTuple[]): Promise { return utils.promiseTry(() => { @@ -487,7 +484,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * Load all user presence events from the database. This is not cached. * FIXME: It would probably be more sensible to store the events in the * sync. - * @return {Promise} A list of presence events in their raw form. + * @returns A list of presence events in their raw form. */ public getUserPresenceEvents(): Promise { return utils.promiseTry(() => { @@ -501,7 +498,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Load all the account data events from the database. This is not cached. - * @return {Promise} A list of raw global account events. + * @returns A list of raw global account events. */ private loadAccountData(): Promise { logger.log(`LocalIndexedDBStoreBackend: loading account data...`); @@ -519,7 +516,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Load the sync data from the database. - * @return {Promise} An object with "roomsData" and "nextBatch" keys. + * @returns An object with "roomsData" and "nextBatch" keys. */ private loadSyncData(): Promise { logger.log(`LocalIndexedDBStoreBackend: loading sync data...`); diff --git a/src/store/indexeddb-remote-backend.ts b/src/store/indexeddb-remote-backend.ts index 1ca6fc03a..05909b0e3 100644 --- a/src/store/indexeddb-remote-backend.ts +++ b/src/store/indexeddb-remote-backend.ts @@ -36,10 +36,9 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { * worker. * * Construct a new Indexed Database store backend. This requires a call to - * connect() before this store can be used. - * @constructor - * @param {Function} workerFactory Factory which produces a Worker - * @param {string=} dbName Optional database name. The same name must be used + * `connect()` before this store can be used. + * @param workerFactory - Factory which produces a Worker + * @param dbName - Optional database name. The same name must be used * to open the same database. */ public constructor( @@ -50,7 +49,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { /** * Attempt to connect to the database. This can fail if the user does not * grant permission. - * @return {Promise} Resolves if successfully connected. + * @returns Promise which resolves if successfully connected. */ public connect(): Promise { return this.ensureStarted().then(() => this.doCmd('connect')); @@ -59,19 +58,19 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { /** * Clear the entire database. This should be used when logging out of a client * to prevent mixing data between accounts. - * @return {Promise} Resolved when the database is cleared. + * @returns Resolved when the database is cleared. */ public clearDatabase(): Promise { return this.ensureStarted().then(() => this.doCmd('clearDatabase')); } - /** @return {Promise} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ public isNewlyCreated(): Promise { return this.doCmd('isNewlyCreated'); } /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ @@ -94,9 +93,8 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ public getOutOfBandMembers(roomId: string): Promise { return this.doCmd('getOutOfBandMembers', [roomId]); @@ -106,9 +104,8 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store - * @returns {Promise} when all members have been stored + * @param membershipEvents - the membership events to store + * @returns when all members have been stored */ public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise { return this.doCmd('setOutOfBandMembers', [roomId, membershipEvents]); @@ -128,7 +125,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { /** * Load all user presence events from the database. This is not cached. - * @return {Promise} A list of presence events in their raw form. + * @returns A list of presence events in their raw form. */ public getUserPresenceEvents(): Promise { return this.doCmd('getUserPresenceEvents'); diff --git a/src/store/indexeddb-store-worker.ts b/src/store/indexeddb-store-worker.ts index 57e7da983..a9bc2abcd 100644 --- a/src/store/indexeddb-store-worker.ts +++ b/src/store/indexeddb-store-worker.ts @@ -27,12 +27,14 @@ interface ICmd { * This class lives in the webworker and drives a LocalIndexedDBStoreBackend * controlled by messages from the main process. * + * @example * It should be instantiated by a web worker script provided by the application * in a script, for example: - * + * ``` * import {IndexedDBStoreWorker} from 'matrix-js-sdk/lib/indexeddb-worker.js'; * const remoteWorker = new IndexedDBStoreWorker(postMessage); * onmessage = remoteWorker.onMessage; + * ``` * * Note that it is advisable to import this class by referencing the file directly to * avoid a dependency on the whole js-sdk. @@ -42,7 +44,7 @@ export class IndexedDBStoreWorker { private backend?: LocalIndexedDBStoreBackend; /** - * @param {function} postMessage The web worker postMessage function that + * @param postMessage - The web worker postMessage function that * should be used to communicate back to the main script. */ public constructor(private readonly postMessage: InstanceType["postMessage"]) {} @@ -51,7 +53,7 @@ export class IndexedDBStoreWorker { * Passes a message event from the main script into the class. This method * can be directly assigned to the web worker `onmessage` variable. * - * @param {Object} ev The message event + * @param ev - The message event */ public onMessage = (ev: MessageEvent): void => { const msg: ICmd = ev.data; diff --git a/src/store/indexeddb.ts b/src/store/indexeddb.ts index 55f8261fa..656049f2b 100644 --- a/src/store/indexeddb.ts +++ b/src/store/indexeddb.ts @@ -32,7 +32,6 @@ import { IStoredClientOpts } from "../client"; /** * This is an internal module. See {@link IndexedDBStore} for the public class. - * @module store/indexeddb */ // If this value is too small we'll be writing very often which will cause @@ -43,8 +42,11 @@ import { IStoredClientOpts } from "../client"; const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes interface IOpts extends IBaseOpts { + /** The Indexed DB interface e.g. `window.indexedDB` */ indexedDB: IDBFactory; + /** Optional database name. The same name must be used to open the same database. */ dbName?: string; + /** Optional factory to spin up a Worker to execute the IDB transactions within. */ workerFactory?: () => Worker; } @@ -57,6 +59,10 @@ export class IndexedDBStore extends MemoryStore { return LocalIndexedDBStoreBackend.exists(indexedDB, dbName); } + /** + * The backend instance. + * Call through to this API if you need to perform specific indexeddb actions like deleting the database. + */ public readonly backend: IIndexedDBBackend; private startedUp = false; @@ -74,10 +80,10 @@ export class IndexedDBStore extends MemoryStore { * the contents of the store to an IndexedDB backend. * * All data is still kept in-memory but can be loaded from disk by calling - * startup(). This can make startup times quicker as a complete + * `startup()`. This can make startup times quicker as a complete * sync from the server is not required. This does not reduce memory usage as all - * the data is eagerly fetched when startup() is called. - *
+     * the data is eagerly fetched when `startup()` is called.
+     * ```
      * let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
      * let store = new IndexedDBStore(opts);
      * await store.startup(); // load from indexed db
@@ -90,24 +96,9 @@ export class IndexedDBStore extends MemoryStore {
      *         console.log("Started up, now with go faster stripes!");
      *     }
      * });
-     * 
+ * ``` * - * @constructor - * @extends MemoryStore - * @param {Object} opts Options object. - * @param {Object} opts.indexedDB The Indexed DB interface e.g. - * window.indexedDB - * @param {string=} opts.dbName Optional database name. The same name must be used - * to open the same database. - * @param {string=} opts.workerScript Optional URL to a script to invoke a web - * worker with to run IndexedDB queries on the web worker. The IndexedDbStoreWorker - * class is provided for this purpose and requires the application to provide a - * trivial wrapper script around it. - * @param {Object=} opts.workerApi The webWorker API object. If omitted, the global Worker - * object will be used if it exists. - * @prop {IndexedDBStoreBackend} backend The backend instance. Call through to - * this API if you need to perform specific indexeddb actions like deleting the - * database. + * @param opts - Options object. */ public constructor(opts: IOpts) { super(opts); @@ -126,7 +117,7 @@ export class IndexedDBStore extends MemoryStore { public on = this.emitter.on.bind(this.emitter); /** - * @return {Promise} Resolved when loaded from indexed db. + * @returns Resolved when loaded from indexed db. */ public startup(): Promise { if (this.startedUp) { @@ -152,7 +143,7 @@ export class IndexedDBStore extends MemoryStore { } /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ @@ -160,13 +151,13 @@ export class IndexedDBStore extends MemoryStore { return this.backend.getSavedSync(); }, "getSavedSync"); - /** @return {Promise} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ public isNewlyCreated = this.degradable((): Promise => { return this.backend.isNewlyCreated(); }, "isNewlyCreated"); /** - * @return {Promise} If there is a saved sync, the nextBatch token + * @returns If there is a saved sync, the nextBatch token * for this sync, otherwise null. */ public getSavedSyncToken = this.degradable((): Promise => { @@ -175,7 +166,7 @@ export class IndexedDBStore extends MemoryStore { /** * Delete all data from this store. - * @return {Promise} Resolves if the data was deleted from the database. + * @returns Promise which resolves if the data was deleted from the database. */ public deleteAllData = this.degradable((): Promise => { super.deleteAllData(); @@ -193,7 +184,7 @@ export class IndexedDBStore extends MemoryStore { * not could change between calling this function and calling * save(). * - * @return {boolean} True if calling save() will actually save + * @returns True if calling save() will actually save * (at the time this function is called). */ public wantsSave(): boolean { @@ -204,8 +195,8 @@ export class IndexedDBStore extends MemoryStore { /** * Possibly write data to the database. * - * @param {boolean} force True to force a save to happen - * @return {Promise} Promise resolves after the write completes + * @param force - True to force a save to happen + * @returns Promise resolves after the write completes * (or immediately if no write is performed) */ public save(force = false): Promise { @@ -241,9 +232,8 @@ export class IndexedDBStore extends MemoryStore { /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ public getOutOfBandMembers = this.degradable((roomId: string): Promise => { return this.backend.getOutOfBandMembers(roomId); @@ -253,9 +243,8 @@ export class IndexedDBStore extends MemoryStore { * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store - * @returns {Promise} when all members have been stored + * @param membershipEvents - the membership events to store + * @returns when all members have been stored */ public setOutOfBandMembers = this.degradable( (roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise => { @@ -287,9 +276,9 @@ export class IndexedDBStore extends MemoryStore { * When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore` * in place so that the current operation and all future ones are in-memory only. * - * @param {Function} func The degradable work to do. - * @param {String} fallback The method name for fallback. - * @returns {Function} A wrapped member function. + * @param func - The degradable work to do. + * @param fallback - The method name for fallback. + * @returns A wrapped member function. */ private degradable, R = void>( func: DegradableFn, @@ -368,8 +357,8 @@ export class IndexedDBStore extends MemoryStore { } /** - * @param {string} roomId ID of the current room - * @returns {string} Storage key to retrieve pending events + * @param roomId - ID of the current room + * @returns Storage key to retrieve pending events */ function pendingEventsKey(roomId: string): string { return `mx_pending_events_${roomId}`; diff --git a/src/store/memory.ts b/src/store/memory.ts index f24ab2d97..c698b9395 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -16,7 +16,6 @@ limitations under the License. /** * This is an internal module. See {@link MemoryStore} for the public class. - * @module store/memory */ import { EventType } from "../@types/event"; @@ -43,16 +42,10 @@ function isValidFilterId(filterId?: string | number | null): boolean { } export interface IOpts { + /** The local storage instance to persist some forms of data such as tokens. Rooms will NOT be stored. */ localStorage?: Storage; } -/** - * Construct a new in-memory data store for the Matrix Client. - * @constructor - * @param {Object=} opts Config options - * @param {Storage} opts.localStorage The local storage instance to persist - * some forms of data such as tokens. Rooms will NOT be stored. - */ export class MemoryStore implements IStore { private rooms: Record = {}; // roomId: Room private users: Record = {}; // userId: User @@ -69,26 +62,30 @@ export class MemoryStore implements IStore { private pendingToDeviceBatches: IndexedToDeviceBatch[] = []; private nextToDeviceBatchId = 0; + /** + * Construct a new in-memory data store for the Matrix Client. + * @param opts - Config options + */ public constructor(opts: IOpts = {}) { this.localStorage = opts.localStorage; } /** * Retrieve the token to stream from. - * @return {string} The token or null. + * @returns The token or null. */ public getSyncToken(): string | null { return this.syncToken; } - /** @return {Promise} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ public isNewlyCreated(): Promise { return Promise.resolve(true); } /** * Set the token to stream from. - * @param {string} token The token to stream from. + * @param token - The token to stream from. */ public setSyncToken(token: string): void { this.syncToken = token; @@ -96,7 +93,7 @@ export class MemoryStore implements IStore { /** * Store the given room. - * @param {Room} room The room to be stored. All properties must be stored. + * @param room - The room to be stored. All properties must be stored. */ public storeRoom(room: Room): void { this.rooms[room.roomId] = room; @@ -112,9 +109,6 @@ export class MemoryStore implements IStore { /** * Called when a room member in a room being tracked by this store has been * updated. - * @param {MatrixEvent} event - * @param {RoomState} state - * @param {RoomMember} member */ private onRoomMember = (event: MatrixEvent | null, state: RoomState, member: RoomMember): void => { if (member.membership === "invite") { @@ -138,8 +132,8 @@ export class MemoryStore implements IStore { /** * Retrieve a room by its' room ID. - * @param {string} roomId The room ID. - * @return {Room} The room or null. + * @param roomId - The room ID. + * @returns The room or null. */ public getRoom(roomId: string): Room | null { return this.rooms[roomId] || null; @@ -147,7 +141,7 @@ export class MemoryStore implements IStore { /** * Retrieve all known rooms. - * @return {Room[]} A list of rooms, which may be empty. + * @returns A list of rooms, which may be empty. */ public getRooms(): Room[] { return Object.values(this.rooms); @@ -155,7 +149,6 @@ export class MemoryStore implements IStore { /** * Permanently delete a room. - * @param {string} roomId */ public removeRoom(roomId: string): void { if (this.rooms[roomId]) { @@ -166,7 +159,7 @@ export class MemoryStore implements IStore { /** * Retrieve a summary of all the rooms. - * @return {RoomSummary[]} A summary of each room. + * @returns A summary of each room. */ public getRoomSummaries(): RoomSummary[] { return Object.values(this.rooms).map(function(room) { @@ -176,7 +169,7 @@ export class MemoryStore implements IStore { /** * Store a User. - * @param {User} user The user to store. + * @param user - The user to store. */ public storeUser(user: User): void { this.users[user.userId] = user; @@ -184,8 +177,8 @@ export class MemoryStore implements IStore { /** * Retrieve a User by its' user ID. - * @param {string} userId The user ID. - * @return {User} The user or null. + * @param userId - The user ID. + * @returns The user or null. */ public getUser(userId: string): User | null { return this.users[userId] || null; @@ -193,7 +186,7 @@ export class MemoryStore implements IStore { /** * Retrieve all known users. - * @return {User[]} A list of users, which may be empty. + * @returns A list of users, which may be empty. */ public getUsers(): User[] { return Object.values(this.users); @@ -201,9 +194,9 @@ export class MemoryStore implements IStore { /** * Retrieve scrollback for this room. - * @param {Room} room The matrix room - * @param {number} limit The max number of old events to retrieve. - * @return {Array} An array of objects which will be at most 'limit' + * @param room - The matrix room + * @param limit - The max number of old events to retrieve. + * @returns An array of objects which will be at most 'limit' * length and at least 0. The objects are the raw event JSON. */ public scrollback(room: Room, limit: number): MatrixEvent[] { @@ -212,18 +205,17 @@ export class MemoryStore implements IStore { /** * Store events for a room. The events have already been added to the timeline - * @param {Room} room The room to store events for. - * @param {Array} events The events to store. - * @param {string} token The token associated with these events. - * @param {boolean} toStart True if these are paginated results. + * @param room - The room to store events for. + * @param events - The events to store. + * @param token - The token associated with these events. + * @param 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. } /** * Store a filter. - * @param {Filter} filter */ public storeFilter(filter: Filter): void { if (!filter?.userId || !filter?.filterId) return; @@ -235,9 +227,7 @@ export class MemoryStore implements IStore { /** * Retrieve a filter. - * @param {string} userId - * @param {string} filterId - * @return {?Filter} A filter or null. + * @returns A filter or null. */ public getFilter(userId: string, filterId: string): Filter | null { if (!this.filters[userId] || !this.filters[userId][filterId]) { @@ -248,8 +238,8 @@ export class MemoryStore implements IStore { /** * Retrieve a filter ID with the given name. - * @param {string} filterName The filter name. - * @return {?string} The filter ID or null. + * @param filterName - The filter name. + * @returns The filter ID or null. */ public getFilterIdByName(filterName: string): string | null { if (!this.localStorage) { @@ -272,8 +262,6 @@ export class MemoryStore implements IStore { /** * Set a filter name to ID mapping. - * @param {string} filterName - * @param {string} filterId */ public setFilterIdByName(filterName: string, filterId?: string): void { if (!this.localStorage) { @@ -293,7 +281,7 @@ export class MemoryStore implements IStore { * Store user-scoped account data events. * N.B. that account data only allows a single event per type, so multiple * events with the same type will replace each other. - * @param {Array} events The events to store. + * @param events - The events to store. */ public storeAccountDataEvents(events: MatrixEvent[]): void { events.forEach((event) => { @@ -303,8 +291,8 @@ export class MemoryStore implements IStore { /** * Get account data event by event type - * @param {string} eventType The event type being queried - * @return {?MatrixEvent} the user account_data event of given type, if any + * @param eventType - The event type being queried + * @returns the user account_data event of given type, if any */ public getAccountData(eventType: EventType | string): MatrixEvent | undefined { return this.accountData[eventType]; @@ -313,8 +301,8 @@ export class MemoryStore implements IStore { /** * setSyncData does nothing as there is no backing data store. * - * @param {Object} syncData The sync data - * @return {Promise} An immediately resolved promise. + * @param syncData - The sync data + * @returns An immediately resolved promise. */ public setSyncData(syncData: ISyncResponse): Promise { return Promise.resolve(); @@ -323,7 +311,7 @@ export class MemoryStore implements IStore { /** * We never want to save becase we have nothing to save to. * - * @return {boolean} If the store wants to save + * @returns If the store wants to save */ public wantsSave(): boolean { return false; @@ -331,21 +319,21 @@ export class MemoryStore implements IStore { /** * Save does nothing as there is no backing data store. - * @param {bool} force True to force a save (but the memory + * @param force - True to force a save (but the memory * store still can't save anything) */ public save(force: boolean): void {} /** * Startup does nothing as this store doesn't require starting up. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ public startup(): Promise { return Promise.resolve(); } /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ @@ -354,7 +342,7 @@ export class MemoryStore implements IStore { } /** - * @return {Promise} If there is a saved sync, the nextBatch token + * @returns If there is a saved sync, the nextBatch token * for this sync, otherwise null. */ public getSavedSyncToken(): Promise { @@ -363,7 +351,7 @@ export class MemoryStore implements IStore { /** * Delete all data from this store. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ public deleteAllData(): Promise { this.rooms = { @@ -387,9 +375,8 @@ export class MemoryStore implements IStore { /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ public getOutOfBandMembers(roomId: string): Promise { return Promise.resolve(this.oobMembers[roomId] || null); @@ -399,9 +386,8 @@ export class MemoryStore implements IStore { * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store - * @returns {Promise} when all members have been stored + * @param membershipEvents - the membership events to store + * @returns when all members have been stored */ public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise { this.oobMembers[roomId] = membershipEvents; diff --git a/src/store/stub.ts b/src/store/stub.ts index 746bc521f..445f9e8ff 100644 --- a/src/store/stub.ts +++ b/src/store/stub.ts @@ -16,7 +16,6 @@ limitations under the License. /** * This is an internal module. - * @module store/stub */ import { EventType } from "../@types/event"; @@ -33,20 +32,18 @@ import { IStoredClientOpts } from "../client"; /** * Construct a stub store. This does no-ops on most store methods. - * @constructor */ export class StubStore implements IStore { public readonly accountData = {}; // stub private fromToken: string | null = null; - /** @return {Promise} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ public isNewlyCreated(): Promise { return Promise.resolve(true); } /** * Get the sync token. - * @return {string} */ public getSyncToken(): string | null { return this.fromToken; @@ -54,7 +51,6 @@ export class StubStore implements IStore { /** * Set the sync token. - * @param {string} token */ public setSyncToken(token: string): void { this.fromToken = token; @@ -62,14 +58,11 @@ export class StubStore implements IStore { /** * No-op. - * @param {Room} room */ public storeRoom(room: Room): void {} /** * No-op. - * @param {string} roomId - * @return {null} */ public getRoom(roomId: string): Room | null { return null; @@ -77,7 +70,7 @@ export class StubStore implements IStore { /** * No-op. - * @return {Array} An empty array. + * @returns An empty array. */ public getRooms(): Room[] { return []; @@ -85,7 +78,6 @@ export class StubStore implements IStore { /** * Permanently delete a room. - * @param {string} roomId */ public removeRoom(roomId: string): void { return; @@ -93,7 +85,7 @@ export class StubStore implements IStore { /** * No-op. - * @return {Array} An empty array. + * @returns An empty array. */ public getRoomSummaries(): RoomSummary[] { return []; @@ -101,14 +93,11 @@ export class StubStore implements IStore { /** * No-op. - * @param {User} user */ public storeUser(user: User): void {} /** * No-op. - * @param {string} userId - * @return {null} */ public getUser(userId: string): User | null { return null; @@ -116,7 +105,6 @@ export class StubStore implements IStore { /** * No-op. - * @return {User[]} */ public getUsers(): User[] { return []; @@ -124,9 +112,6 @@ export class StubStore implements IStore { /** * No-op. - * @param {Room} room - * @param {number} limit - * @return {Array} */ public scrollback(room: Room, limit: number): MatrixEvent[] { return []; @@ -134,24 +119,21 @@ export class StubStore implements IStore { /** * Store events for a room. - * @param {Room} room The room to store events for. - * @param {Array} events The events to store. - * @param {string} token The token associated with these events. - * @param {boolean} toStart True if these are paginated results. + * @param room - The room to store events for. + * @param events - The events to store. + * @param token - The token associated with these events. + * @param 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. - * @param {Filter} filter */ public storeFilter(filter: Filter): void {} /** * Retrieve a filter. - * @param {string} userId - * @param {string} filterId - * @return {?Filter} A filter or null. + * @returns A filter or null. */ public getFilter(userId: string, filterId: string): Filter | null { return null; @@ -159,8 +141,8 @@ export class StubStore implements IStore { /** * Retrieve a filter ID with the given name. - * @param {string} filterName The filter name. - * @return {?string} The filter ID or null. + * @param filterName - The filter name. + * @returns The filter ID or null. */ public getFilterIdByName(filterName: string): string | null { return null; @@ -168,20 +150,18 @@ export class StubStore implements IStore { /** * Set a filter name to ID mapping. - * @param {string} filterName - * @param {string} filterId */ public setFilterIdByName(filterName: string, filterId?: string): void {} /** * Store user-scoped account data events - * @param {Array} events The events to store. + * @param events - The events to store. */ public storeAccountDataEvents(events: MatrixEvent[]): void {} /** * Get account data event by event type - * @param {string} eventType The event type being queried + * @param eventType - The event type being queried */ public getAccountData(eventType: EventType | string): MatrixEvent | undefined { return undefined; @@ -190,8 +170,8 @@ export class StubStore implements IStore { /** * setSyncData does nothing as there is no backing data store. * - * @param {Object} syncData The sync data - * @return {Promise} An immediately resolved promise. + * @param syncData - The sync data + * @returns An immediately resolved promise. */ public setSyncData(syncData: ISyncResponse): Promise { return Promise.resolve(); @@ -200,7 +180,7 @@ export class StubStore implements IStore { /** * We never want to save because we have nothing to save to. * - * @return {boolean} If the store wants to save + * @returns If the store wants to save */ public wantsSave(): boolean { return false; @@ -213,14 +193,14 @@ export class StubStore implements IStore { /** * Startup does nothing. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ public startup(): Promise { return Promise.resolve(); } /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ @@ -229,7 +209,7 @@ export class StubStore implements IStore { } /** - * @return {Promise} If there is a saved sync, the nextBatch token + * @returns If there is a saved sync, the nextBatch token * for this sync, otherwise null. */ public getSavedSyncToken(): Promise { @@ -239,7 +219,7 @@ export class StubStore implements IStore { /** * Delete all data from this store. Does nothing since this store * doesn't store anything. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ public deleteAllData(): Promise { return Promise.resolve(); diff --git a/src/sync-accumulator.ts b/src/sync-accumulator.ts index 19d5cf131..13f87ecc0 100644 --- a/src/sync-accumulator.ts +++ b/src/sync-accumulator.ts @@ -16,7 +16,6 @@ limitations under the License. /** * This is an internal module. See {@link SyncAccumulator} for the public class. - * @module sync-accumulator */ import { logger } from './logger'; @@ -28,6 +27,13 @@ import { MAIN_ROOM_TIMELINE, ReceiptContent, ReceiptType } from "./@types/read_r import { UNREAD_THREAD_NOTIFICATIONS } from './@types/sync'; interface IOpts { + /** + * The ideal maximum number of timeline entries to keep in the sync response. + * This is best-effort, as clients do not always have a back-pagination token for each event, + * so it's possible there may be slightly *less* than this value. There will never be more. + * This cannot be 0 or else it makes it impossible to scroll back in a room. + * Default: 50. + */ maxTimelineEntries?: number; } @@ -116,7 +122,7 @@ interface IAccountData { events: IMinimalEvent[]; } -interface IToDeviceEvent { +export interface IToDeviceEvent { content: IContent; sender: string; type: string; @@ -127,8 +133,8 @@ interface IToDevice { } interface IDeviceLists { - changed: string[]; - left: string[]; + changed?: string[]; + left?: string[]; } export interface ISyncResponse { @@ -139,6 +145,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 +191,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 @@ -202,15 +217,6 @@ export class SyncAccumulator { // streaming from without losing events. private nextBatch: string | null = null; - /** - * @param {Object} opts - * @param {Number=} opts.maxTimelineEntries The ideal maximum number of - * timeline entries to keep in the sync response. This is best-effort, as - * clients do not always have a back-pagination token for each event, so - * it's possible there may be slightly *less* than this value. There will - * never be more. This cannot be 0 or else it makes it impossible to scroll - * back in a room. Default: 50. - */ public constructor(private readonly opts: IOpts = {}) { this.opts.maxTimelineEntries = this.opts.maxTimelineEntries || 50; } @@ -233,8 +239,8 @@ export class SyncAccumulator { /** * Accumulate incremental /sync room data. - * @param {Object} syncResponse the complete /sync JSON - * @param {boolean} fromDatabase True if the sync response is one saved to the database + * @param syncResponse - the complete /sync JSON + * @param fromDatabase - True if the sync response is one saved to the database */ private accumulateRooms(syncResponse: ISyncResponse, fromDatabase = false): void { if (!syncResponse.rooms) { @@ -473,35 +479,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. @@ -526,8 +528,8 @@ export class SyncAccumulator { * represents all room data that should be stored. This should be paired * with the sync token which represents the most recent /sync response * provided to accumulate(). - * @param {boolean} forDatabase True to generate a sync to be saved to storage - * @return {Object} An object with a "nextBatch", "roomsData" and "accountData" + * @param forDatabase - True to generate a sync to be saved to storage + * @returns An object with a "nextBatch", "roomsData" and "accountData" * keys. * The "nextBatch" key is a string which represents at what point in the * /sync stream the accumulator reached. This token should be used when @@ -581,7 +583,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 +628,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 +643,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 f32ccf7d2..38325ca83 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -106,7 +106,8 @@ function getFilterName(userId: string, suffix?: string): string { return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : ""); } -function debuglog(...params): void { +/* istanbul ignore next */ +function debuglog(...params: any[]): void { if (!DEBUG) return; logger.log(...params); } @@ -117,9 +118,27 @@ interface ISyncOptions { } export interface ISyncStateData { + /** + * The matrix error if `state=ERROR`. + */ error?: Error; + /** + * The 'since' token passed to /sync. + * `null` for the first successful sync since this client was + * started. Only present if `state=PREPARED` or + * `state=SYNCING`. + */ oldSyncToken?: string; + /** + * The 'next_batch' result from /sync, which + * will become the 'since' token for the next call to /sync. Only present if + * `state=PREPARED or state=SYNCING`. + */ nextSyncToken?: string; + /** + * True if we are working our way through a + * backlog of events after connecting. Only present if `state=SYNCING`. + */ catchingUp?: boolean; fromCache?: boolean; } @@ -146,21 +165,6 @@ type WrappedRoom = T & { isBrandNewRoom: boolean; }; -/** - * Internal class - unstable. - * Construct an entity which is able to sync with a homeserver. - * @constructor - * @param {MatrixClient} client The matrix client instance to use. - * @param {Object} opts Config options - * @param {module:crypto=} opts.crypto Crypto manager - * @param {Function=} opts.canResetEntireTimeline A function which is called - * with a room ID and returns a boolean. It should return 'true' if the SDK can - * SAFELY remove events from this room. It may not be safe to remove events if - * there are other references to the timelines for this room. - * Default: returns false. - * @param {Boolean=} opts.disablePresence True to perform syncing without automatically - * updating presence. - */ export class SyncApi { private _peekRoom: Optional = null; private currentSyncRequest?: Promise; @@ -175,6 +179,12 @@ export class SyncApi { private failedSyncCount = 0; // Number of consecutive failed /sync requests private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start + /** + * Construct an entity which is able to sync with a homeserver. + * @param client - The matrix client instance to use. + * @param opts - Config options + * @internal + */ public constructor(private readonly client: MatrixClient, private readonly opts: Partial = {}) { this.opts.initialSyncLimit = this.opts.initialSyncLimit ?? 8; this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false; @@ -196,10 +206,6 @@ export class SyncApi { } } - /** - * @param {string} roomId - * @return {Room} - */ public createRoom(roomId: string): Room { const room = _createAndReEmitRoom(this.client, roomId, this.opts); @@ -214,9 +220,9 @@ export class SyncApi { * new historical messages imported by MSC2716 `/batch_send` somewhere in * the room and we need to throw away the timeline to make sure the * historical messages are shown when we paginate `/messages` again. - * @param {Room} room The room where the marker event was sent - * @param {MatrixEvent} markerEvent The new marker event - * @param {IMarkerFoundOptions} setStateOptions When `timelineWasEmpty` is set + * @param room - The room where the marker event was sent + * @param markerEvent - The new marker event + * @param setStateOptions - When `timelineWasEmpty` is set * as `true`, the given marker event will be ignored */ private onMarkerStateEvent( @@ -279,7 +285,7 @@ export class SyncApi { /** * Sync rooms the user has left. - * @return {Promise} Resolved when they've been added to the store. + * @returns Resolved when they've been added to the store. */ public async syncLeftRooms(): Promise { const client = this.client; @@ -350,8 +356,8 @@ export class SyncApi { /** * Peek into a room. This will result in the room in question being synced so it * is accessible via getRooms(). Live updates for the room will be provided. - * @param {string} roomId The room ID to peek into. - * @return {Promise} A promise which resolves once the room has been added to the + * @param roomId - The room ID to peek into. + * @returns A promise which resolves once the room has been added to the * store. */ public peek(roomId: string): Promise { @@ -430,8 +436,7 @@ export class SyncApi { /** * Do a peek room poll. - * @param {Room} peekRoom - * @param {string?} token from= token + * @param token - from= token */ private peekPoll(peekRoom: Room, token?: string): void { if (this._peekRoom !== peekRoom) { @@ -494,8 +499,7 @@ export class SyncApi { /** * Returns the current state of this sync object - * @see module:client~MatrixClient#event:"sync" - * @return {?String} + * @see MatrixClient#event:"sync" */ public getSyncState(): SyncState | null { return this.syncState; @@ -507,7 +511,6 @@ export class SyncApi { * such data. * Sync errors, if available, are put in the 'error' key of * this object. - * @return {?Object} */ public getSyncStateData(): ISyncStateData | null { return this.syncStateData ?? null; @@ -526,8 +529,8 @@ export class SyncApi { /** * Is the lazy loading option different than in previous session? - * @param {boolean} lazyLoadMembers current options for lazy loading - * @return {boolean} whether or not the option has changed compared to the previous session */ + * @param lazyLoadMembers - current options for lazy loading + * @returns whether or not the option has changed compared to the previous session */ private async wasLazyLoadingToggled(lazyLoadMembers = false): Promise { // assume it was turned off before // if we don't know any better @@ -758,7 +761,7 @@ export class SyncApi { /** * Retry a backed off syncing request immediately. This should only be used when * the user explicitly attempts to retry their lost connection. - * @return {boolean} True if this resulted in a request being retried. + * @returns True if this resulted in a request being retried. */ public retryImmediately(): boolean { if (!this.connectionReturnedDefer) { @@ -769,7 +772,7 @@ export class SyncApi { } /** * Process a single set of cached sync data. - * @param {Object} savedSync a saved sync that was persisted by a store. This + * @param savedSync - a saved sync that was persisted by a store. This * should have been acquired via client.store.getSavedSync(). */ private async syncFromCache(savedSync: ISavedSync): Promise { @@ -811,9 +814,6 @@ export class SyncApi { /** * Invoke me to do /sync calls - * @param {Object} syncOptions - * @param {string} syncOptions.filterId - * @param {boolean} syncOptions.hasSyncedBefore */ private async doSync(syncOptions: ISyncOptions): Promise { while (this.running) { @@ -1023,8 +1023,8 @@ export class SyncApi { * Process data returned from a sync response and propagate it * into the model objects * - * @param {Object} syncEventData Object containing sync tokens associated with this sync - * @param {Object} data The response from /sync + * @param syncEventData - Object containing sync tokens associated with this sync + * @param data - The response from /sync */ private async processSyncResponse(syncEventData: ISyncStateData, data: ISyncResponse): Promise { const client = this.client; @@ -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; }, {}); @@ -1362,6 +1362,18 @@ export class SyncApi { } } + // process any crypto events *before* emitting the RoomStateEvent events. This + // avoids a race condition if the application tries to send a message after the + // state event is processed, but before crypto is enabled, which then causes the + // crypto layer to complain. + if (this.opts.crypto) { + for (const e of stateEvents.concat(events)) { + if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") { + await this.opts.crypto.onCryptoEvent(room, e); + } + } + } + try { await this.injectRoomEvents(room, stateEvents, events, syncEventData.fromCache); } catch (e) { @@ -1389,21 +1401,11 @@ export class SyncApi { this.processEventsForNotifs(room, events); - const processRoomEvent = async (e): Promise => { - client.emit(ClientEvent.Event, e); - if (e.isState() && e.getType() == "m.room.encryption" && this.opts.crypto) { - await this.opts.crypto.onCryptoEvent(e); - } - }; - - await utils.promiseMapSeries(stateEvents, processRoomEvent); - await utils.promiseMapSeries(events, processRoomEvent); - ephemeralEvents.forEach(function(e) { - client.emit(ClientEvent.Event, e); - }); - accountDataEvents.forEach(function(e) { - client.emit(ClientEvent.Event, e); - }); + const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e); + stateEvents.forEach(emitEvent); + events.forEach(emitEvent); + ephemeralEvents.forEach(emitEvent); + accountDataEvents.forEach(emitEvent); // Decrypt only the last message in all rooms to make sure we can generate a preview // And decrypt all events after the recorded read receipt to ensure an accurate @@ -1471,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) && @@ -1487,10 +1490,10 @@ export class SyncApi { /** * Starts polling the connectivity check endpoint - * @param {number} delay How long to delay until the first poll. + * @param delay - How long to delay until the first poll. * defaults to a short, randomised interval (to prevent * tight-looping if /versions succeeds but /sync etc. fail). - * @return {promise} which resolves once the connection returns + * @returns which resolves once the connection returns */ private startKeepAlives(delay?: number): Promise { if (delay === undefined) { @@ -1518,7 +1521,7 @@ export class SyncApi { * On failure, schedules a call back to itself. On success, resolves * this.connectionReturnedDefer. * - * @param {boolean} connDidFail True if a connectivity failure has been detected. Optional. + * @param connDidFail - True if a connectivity failure has been detected. Optional. */ private pokeKeepAlive(connDidFail = false): void { const success = (): void => { @@ -1565,10 +1568,6 @@ export class SyncApi { }); } - /** - * @param {Object} obj - * @return {Object[]} - */ private mapSyncResponseToRoomArray( obj: Record, ): Array> { @@ -1590,12 +1589,6 @@ export class SyncApi { }); } - /** - * @param {Object} obj - * @param {Room} room - * @param {boolean} decrypt - * @return {MatrixEvent[]} - */ private mapSyncEventsFormat( obj: IInviteState | ITimeline | IEphemeral, room?: Room, @@ -1605,16 +1598,16 @@ 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); }); } /** - * @param {Room} room */ private resolveInvites(room: Room): void { if (!room || !this.opts.resolveInvitesToProfiles) { @@ -1658,12 +1651,11 @@ export class SyncApi { /** * Injects events into a room's model. - * @param {Room} room - * @param {MatrixEvent[]} stateEventList A list of state events. This is the state + * @param stateEventList - A list of state events. This is the state * at the *START* of the timeline list if it is supplied. - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events, including threaded. Lower index + * @param timelineEventList - A list of timeline events, including threaded. Lower index * is earlier in time. Higher index is later. - * @param {boolean} fromCache whether the sync response came from cache + * @param fromCache - whether the sync response came from cache */ public async injectRoomEvents( room: Room, @@ -1739,8 +1731,7 @@ export class SyncApi { * as appropriate. * This must be called after the room the events belong to has been stored. * - * @param {Room} room - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index + * @param timelineEventList - A list of timeline events. Lower index * is earlier in time. Higher index is later. */ private processEventsForNotifs(room: Room, timelineEventList: MatrixEvent[]): void { @@ -1755,9 +1746,6 @@ export class SyncApi { } } - /** - * @return {string} - */ private getGuestFilter(): string { // Dev note: This used to be conditional to return a filter of 20 events maximum, but // the condition never went to the other branch. This is now hardcoded. @@ -1766,8 +1754,8 @@ export class SyncApi { /** * Sets the sync state and emits an event to say so - * @param {String} newState The new state string - * @param {Object} data Object of additional data to emit in the event + * @param newState - The new state string + * @param data - Object of additional data to emit in the event */ private updateSyncState(newState: SyncState, data?: ISyncStateData): void { const old = this.syncState; diff --git a/src/timeline-window.ts b/src/timeline-window.ts index 5498600a9..2d0c9da15 100644 --- a/src/timeline-window.ts +++ b/src/timeline-window.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** @module timeline-window */ - import { Optional } from "matrix-events-sdk"; import { Direction, EventTimeline } from './models/event-timeline'; @@ -25,23 +23,28 @@ import { EventTimelineSet } from "./models/event-timeline-set"; import { MatrixEvent } from "./models/event"; /** - * @private + * @internal */ const DEBUG = false; /** - * @private + * @internal */ +/* istanbul ignore next */ const debuglog = DEBUG ? logger.log.bind(logger) : function(): void {}; /** * the number of times we ask the server for more events before giving up * - * @private + * @internal */ const DEFAULT_PAGINATE_LOOP_LIMIT = 5; interface IOpts { + /** + * Maximum number of events to keep in the window. If more events are retrieved via pagination requests, + * excess events will be dropped from the other end of the window. + */ windowLimit?: number; } @@ -58,31 +61,22 @@ export class TimelineWindow { /** * Construct a TimelineWindow. * - *

This abstracts the separate timelines in a Matrix {@link - * module:models/room|Room} into a single iterable thing. It keeps track of - * the start and endpoints of the window, which can be advanced with the help + *

This abstracts the separate timelines in a Matrix {@link Room} into a single iterable thing. + * It keeps track of the start and endpoints of the window, which can be advanced with the help * of pagination requests. * - *

Before the window is useful, it must be initialised by calling {@link - * module:timeline-window~TimelineWindow#load|load}. + *

Before the window is useful, it must be initialised by calling {@link TimelineWindow#load}. * *

Note that the window will not automatically extend itself when new events - * are received from /sync; you should arrange to call {@link - * module:timeline-window~TimelineWindow#paginate|paginate} on {@link - * module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events. + * are received from /sync; you should arrange to call {@link TimelineWindow#paginate} + * on {@link RoomEvent.Timeline} events. * - * @param {MatrixClient} client MatrixClient to be used for context/pagination + * @param client - MatrixClient to be used for context/pagination * requests. * - * @param {EventTimelineSet} timelineSet The timelineSet to track + * @param timelineSet - The timelineSet to track * - * @param {Object} [opts] Configuration options for this window - * - * @param {number} [opts.windowLimit = 1000] maximum number of events to keep - * in the window. If more events are retrieved via pagination requests, - * excess events will be dropped from the other end of the window. - * - * @constructor + * @param opts - Configuration options for this window */ public constructor( private readonly client: MatrixClient, @@ -95,11 +89,9 @@ export class TimelineWindow { /** * Initialise the window to point at a given event, or the live timeline * - * @param {string} [initialEventId] If given, the window will contain the + * @param initialEventId - If given, the window will contain the * given event - * @param {number} [initialWindowSize = 20] Size of the initial window - * - * @return {Promise} + * @param initialWindowSize - Size of the initial window */ public load(initialEventId?: string, initialWindowSize = 20): Promise { // given an EventTimeline, find the event we were looking for, and initialise our @@ -148,11 +140,11 @@ export class TimelineWindow { /** * Get the TimelineIndex of the window in the given direction. * - * @param {string} direction EventTimeline.BACKWARDS to get the TimelineIndex + * @param direction - EventTimeline.BACKWARDS to get the TimelineIndex * at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at * the end. * - * @return {TimelineIndex} The requested timeline index if one exists, null + * @returns The requested timeline index if one exists, null * otherwise. */ public getTimelineIndex(direction: Direction): TimelineIndex | null { @@ -169,11 +161,11 @@ export class TimelineWindow { * Try to extend the window using events that are already in the underlying * TimelineIndex. * - * @param {string} direction EventTimeline.BACKWARDS to try extending it + * @param direction - EventTimeline.BACKWARDS to try extending it * backwards; EventTimeline.FORWARDS to try extending it forwards. - * @param {number} size number of events to try to extend by. + * @param size - number of events to try to extend by. * - * @return {boolean} true if the window was extended, false otherwise. + * @returns true if the window was extended, false otherwise. */ public extend(direction: Direction, size: number): boolean { const tl = this.getTimelineIndex(direction); @@ -209,10 +201,10 @@ export class TimelineWindow { * necessarily mean that there are more events available in that direction at * this time. * - * @param {string} direction EventTimeline.BACKWARDS to check if we can + * @param direction - EventTimeline.BACKWARDS to check if we can * paginate backwards; EventTimeline.FORWARDS to check if we can go forwards * - * @return {boolean} true if we can paginate in the given direction + * @returns true if we can paginate in the given direction */ public canPaginate(direction: Direction): boolean { const tl = this.getTimelineIndex(direction); @@ -240,23 +232,23 @@ export class TimelineWindow { /** * Attempt to extend the window * - * @param {string} direction EventTimeline.BACKWARDS to extend the window + * @param direction - EventTimeline.BACKWARDS to extend the window * backwards (towards older events); EventTimeline.FORWARDS to go forwards. * - * @param {number} size number of events to try to extend by. If fewer than this + * @param size - number of events to try to extend by. If fewer than this * number are immediately available, then we return immediately rather than * making an API call. * - * @param {boolean} [makeRequest = true] whether we should make API calls to + * @param makeRequest - whether we should make API calls to * fetch further events if we don't have any at all. (This has no effect if * the room already knows about additional events in the relevant direction, * even if there are fewer than 'size' of them, as we will just return those * we already know about.) * - * @param {number} [requestLimit = 5] limit for the number of API requests we + * @param requestLimit - limit for the number of API requests we * should make. * - * @return {Promise} Resolves to a boolean which is true if more events + * @returns Promise which resolves to a boolean which is true if more events * were successfully retrieved. */ public async paginate( @@ -330,8 +322,8 @@ export class TimelineWindow { /** * Remove `delta` events from the start or end of the timeline. * - * @param {number} delta number of events to remove from the timeline - * @param {boolean} startOfTimeline if events should be removed from the start + * @param delta - number of events to remove from the timeline + * @param startOfTimeline - if events should be removed from the start * of the timeline. */ public unpaginate(delta: number, startOfTimeline: boolean): void { @@ -368,7 +360,7 @@ export class TimelineWindow { /** * Get a list of the events currently in the window * - * @return {MatrixEvent[]} the events in the window + * @returns the events in the window */ public getEvents(): MatrixEvent[] { if (!this.start) { @@ -419,12 +411,8 @@ export class TimelineWindow { } /** - * a thing which contains a timeline reference, and an index into it. - * - * @constructor - * @param {EventTimeline} timeline - * @param {number} index - * @private + * A thing which contains a timeline reference, and an index into it. + * @internal */ export class TimelineIndex { public pendingPaginate?: Promise; @@ -433,7 +421,7 @@ export class TimelineIndex { public constructor(public timeline: EventTimeline, public index: number) {} /** - * @return {number} the minimum possible value for the index in the current + * @returns the minimum possible value for the index in the current * timeline */ public minIndex(): number { @@ -441,7 +429,7 @@ export class TimelineIndex { } /** - * @return {number} the maximum possible value for the index in the current + * @returns the maximum possible value for the index in the current * timeline (exclusive - ie, it actually returns one more than the index * of the last element). */ @@ -452,8 +440,8 @@ export class TimelineIndex { /** * Try move the index forward, or into the neighbouring timeline * - * @param {number} delta number of events to advance by - * @return {number} number of events successfully advanced by + * @param delta - number of events to advance by + * @returns number of events successfully advanced by */ public advance(delta: number): number { if (!delta) { @@ -512,8 +500,8 @@ export class TimelineIndex { /** * Try move the index backwards, or into the neighbouring timeline * - * @param {number} delta number of events to retreat by - * @return {number} number of events successfully retreated by + * @param delta - number of events to retreat by + * @returns number of events successfully retreated by */ public retreat(delta: number): number { return this.advance(delta * -1) * -1; diff --git a/src/utils.ts b/src/utils.ts index 71871d3b7..8cf248c78 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -17,7 +17,6 @@ limitations under the License. /** * This is an internal module. - * @module utils */ import unhomoglyph from "unhomoglyph"; @@ -33,7 +32,7 @@ const interns = new Map(); /** * Internalises a string, reusing a known pointer or storing the pointer * if needed for future strings. - * @param str The string to internalise. + * @param str - The string to internalise. * @returns The internalised string. */ export function internaliseString(str: string): string { @@ -55,9 +54,9 @@ export function internaliseString(str: string): string { /** * Encode a dictionary of query parameters. * Omits any undefined/null values. - * @param {Object} params A dict of key/values to encode e.g. - * {"foo": "bar", "baz": "taz"} - * @return {string} The encoded string e.g. foo=bar&baz=taz + * @param params - A dict of key/values to encode e.g. + * `{"foo": "bar", "baz": "taz"}` + * @returns The encoded string e.g. foo=bar&baz=taz */ export function encodeParams(params: QueryDict, urlSearchParams?: URLSearchParams): URLSearchParams { const searchParams = urlSearchParams ?? new URLSearchParams(); @@ -79,9 +78,6 @@ export type QueryDict = Record { /** * Encodes a URI according to a set of template variables. Variables will be * passed through encodeURIComponent. - * @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'. - * @param {Object} variables The key/value pairs to replace the template - * variables with. E.g. { "$bar": "baz" }. - * @return {string} The result of replacing all template variables e.g. '/foo/baz'. + * @param pathTemplate - The path with template variables e.g. '/foo/$bar'. + * @param variables - The key/value pairs to replace the template + * variables with. E.g. `{ "$bar": "baz" }`. + * @returns The result of replacing all template variables e.g. '/foo/baz'. */ export function encodeUri(pathTemplate: string, variables: Record>): string { for (const key in variables) { @@ -142,12 +138,12 @@ export function encodeUri(pathTemplate: string, variables: Recordfn(element, index, array). Return true to + * @param array - The array. + * @param fn - Function to execute on each value in the array, with the + * function signature `fn(element, index, array)`. Return true to * remove this element and break. - * @param {boolean} reverse True to search in reverse order. - * @return {boolean} True if an element was removed. + * @param reverse - True to search in reverse order. + * @returns True if an element was removed. */ export function removeElement( array: T[], @@ -175,8 +171,8 @@ export function removeElement( /** * Checks if the given thing is a function. - * @param {*} value The thing to check. - * @return {boolean} True if it is a function. + * @param value - The thing to check. + * @returns True if it is a function. */ export function isFunction(value: any): boolean { return Object.prototype.toString.call(value) === "[object Function]"; @@ -184,8 +180,8 @@ export function isFunction(value: any): boolean { /** * Checks that the given object has the specified keys. - * @param {Object} obj The object to check. - * @param {string[]} keys The list of keys that 'obj' must have. + * @param obj - The object to check. + * @param keys - The list of keys that 'obj' must have. * @throws If the object is missing keys. */ // note using 'keys' here would shadow the 'keys' function defined above @@ -200,8 +196,8 @@ export function checkObjectHasKeys(obj: object, keys: string[]): void { /** * Deep copy the given object. The object MUST NOT have circular references and * MUST NOT have functions. - * @param {Object} obj The object to deep copy. - * @return {Object} A copy of the object without any references to the original. + * @param obj - The object to deep copy. + * @returns A copy of the object without any references to the original. */ export function deepCopy(obj: T): T { return JSON.parse(JSON.stringify(obj)); @@ -210,10 +206,10 @@ export function deepCopy(obj: T): T { /** * Compare two objects for equality. The objects MUST NOT have circular references. * - * @param {Object} x The first object to compare. - * @param {Object} y The second object to compare. + * @param x - The first object to compare. + * @param y - The second object to compare. * - * @return {boolean} true if the two objects are equal + * @returns true if the two objects are equal */ export function deepCompare(x: any, y: any): boolean { // Inspired by @@ -290,8 +286,8 @@ export function deepCompare(x: any, y: any): boolean { * sorts the result by key, recursively. The input object must * ensure it does not have loops. If the input is not an object * then it will be returned as-is. - * @param {*} obj The object to get entries of - * @returns {Array} The entries, sorted by key. + * @param obj - The object to get entries of + * @returns The entries, sorted by key. */ export function deepSortedObjectEntries(obj: any): [string, any][] { if (typeof(obj) !== "object") return obj; @@ -313,18 +309,18 @@ export function deepSortedObjectEntries(obj: any): [string, any][] { /** * Returns whether the given value is a finite number without type-coercion * - * @param {*} value the value to test - * @return {boolean} whether or not value is a finite number without type-coercion + * @param value - the value to test + * @returns 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); } /** * Removes zero width chars, diacritics and whitespace from the string * Also applies an unhomoglyph on the string, to prevent similar looking chars - * @param {string} str the string to remove hidden characters from - * @return {string} a string with the hidden characters removed + * @param str - the string to remove hidden characters from + * @returns a string with the hidden characters removed */ export function removeHiddenChars(str: string): string { if (typeof str === "string") { @@ -335,7 +331,6 @@ export function removeHiddenChars(str: string): string { /** * Removes the direction override characters from a string - * @param {string} input * @returns string with chars removed */ export function removeDirectionOverrideChars(str: string): string { @@ -428,8 +423,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; @@ -466,9 +461,9 @@ export async function chunkPromises(fns: (() => Promise)[], chunkSize: num * a promise which throws/rejects on error, otherwise the retry will assume the request * succeeded. The promise chain returned will contain the successful promise. The given function * should always return a new promise. - * @param {Function} promiseFn The function to call to get a fresh promise instance. Takes an + * @param promiseFn - The function to call to get a fresh promise instance. Takes an * attempt count as an argument, for logging/debugging purposes. - * @returns {Promise} The promise for the retried operation. + * @returns The promise for the retried operation. */ export function simpleRetryOperation(promiseFn: (attempt: number) => Promise): Promise { return promiseRetry((attempt: number) => { @@ -503,10 +498,10 @@ export const DEFAULT_ALPHABET = ((): string => { * padded at the end with the first character in the alphabet. * * This is intended for use with string averaging. - * @param {string} s The string to pad. - * @param {number} n The length to pad to. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The padded string. + * @param s - The string to pad. + * @param n - The length to pad to. + * @param alphabet - The alphabet to use as a single string. + * @returns The padded string. */ export function alphabetPad(s: string, n: number, alphabet = DEFAULT_ALPHABET): string { return s.padEnd(n, alphabet[0]); @@ -516,9 +511,9 @@ export function alphabetPad(s: string, n: number, alphabet = DEFAULT_ALPHABET): * Converts a baseN number to a string, where N is the alphabet's length. * * This is intended for use with string averaging. - * @param {bigint} n The baseN number. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The baseN number encoded as a string from the alphabet. + * @param n - The baseN number. + * @param alphabet - The alphabet to use as a single string. + * @returns The baseN number encoded as a string from the alphabet. */ export function baseToString(n: bigint, alphabet = DEFAULT_ALPHABET): string { // Developer note: the stringToBase() function offsets the character set by 1 so that repeated @@ -550,9 +545,9 @@ export function baseToString(n: bigint, alphabet = DEFAULT_ALPHABET): string { * Converts a string to a baseN number, where N is the alphabet's length. * * This is intended for use with string averaging. - * @param {string} s The string to convert to a number. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {bigint} The baseN number. + * @param s - The string to convert to a number. + * @param alphabet - The alphabet to use as a single string. + * @returns The baseN number. */ export function stringToBase(s: string, alphabet = DEFAULT_ALPHABET): bigint { const len = BigInt(alphabet.length); @@ -584,10 +579,10 @@ export function stringToBase(s: string, alphabet = DEFAULT_ALPHABET): bigint { * Averages two strings, returning the midpoint between them. This is accomplished by * converting both to baseN numbers (where N is the alphabet's length) then averaging * those before re-encoding as a string. - * @param {string} a The first string. - * @param {string} b The second string. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The midpoint between the strings, as a string. + * @param a - The first string. + * @param b - The second string. + * @param alphabet - The alphabet to use as a single string. + * @returns The midpoint between the strings, as a string. */ export function averageBetweenStrings(a: string, b: string, alphabet = DEFAULT_ALPHABET): string { const padN = Math.max(a.length, b.length); @@ -608,9 +603,9 @@ export function averageBetweenStrings(a: string, b: string, alphabet = DEFAULT_A * Finds the next string using the alphabet provided. This is done by converting the * string to a baseN number, where N is the alphabet's length, then adding 1 before * converting back to a string. - * @param {string} s The string to start at. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The string which follows the input string. + * @param s - The string to start at. + * @param alphabet - The alphabet to use as a single string. + * @returns The string which follows the input string. */ export function nextString(s: string, alphabet = DEFAULT_ALPHABET): string { return baseToString(stringToBase(s, alphabet) + BigInt(1), alphabet); @@ -620,9 +615,9 @@ export function nextString(s: string, alphabet = DEFAULT_ALPHABET): string { * Finds the previous string using the alphabet provided. This is done by converting the * string to a baseN number, where N is the alphabet's length, then subtracting 1 before * converting back to a string. - * @param {string} s The string to start at. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The string which precedes the input string. + * @param s - The string to start at. + * @param alphabet - The alphabet to use as a single string. + * @returns The string which precedes the input string. */ export function prevString(s: string, alphabet = DEFAULT_ALPHABET): string { return baseToString(stringToBase(s, alphabet) - BigInt(1), alphabet); @@ -630,9 +625,9 @@ export function prevString(s: string, alphabet = DEFAULT_ALPHABET): string { /** * Compares strings lexicographically as a sort-safe function. - * @param {string} a The first (reference) string. - * @param {string} b The second (compare) string. - * @returns {number} Negative if the reference string is before the compare string; + * @param a - The first (reference) string. + * @param b - The second (compare) string. + * @returns Negative if the reference string is before the compare string; * positive if the reference string is after; and zero if equal. */ export function lexicographicCompare(a: string, b: string): number { @@ -650,8 +645,8 @@ export function lexicographicCompare(a: string, b: string): number { const collator = new Intl.Collator(); /** * Performant language-sensitive string comparison - * @param a the first string to compare - * @param b the second string to compare + * @param a - the first string to compare + * @param b - the second string to compare */ export function compare(a: string, b: string): number { return collator.compare(a, b); @@ -661,22 +656,24 @@ export function compare(a: string, b: string): number { * This function is similar to Object.assign() but it assigns recursively and * allows you to ignore nullish values from the source * - * @param {Object} target - * @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 { @@ -697,7 +694,7 @@ export function isSupportedReceiptType(receiptType: string): boolean { /** * Determines whether two maps are equal. - * @param eq The equivalence relation to compare values by. Defaults to strict equality. + * @param eq - The equivalence relation to compare values by. Defaults to strict equality. */ export function mapsEqual(x: Map, y: Map, eq = (v1: V, v2: V): boolean => v1 === v2): boolean { if (x.size !== y.size) return false; diff --git a/src/webrtc/audioContext.ts b/src/webrtc/audioContext.ts index 0e08574b8..7cf3ed3f6 100644 --- a/src/webrtc/audioContext.ts +++ b/src/webrtc/audioContext.ts @@ -22,7 +22,7 @@ let refCount = 0; * It's highly recommended to reuse this AudioContext rather than creating your * own, because multiple AudioContexts can be problematic in some browsers. * Make sure to call releaseContext when you're done using it. - * @returns {AudioContext} The shared AudioContext + * @returns The shared AudioContext */ export const acquireContext = (): AudioContext => { if (audioContext === null) audioContext = new AudioContext(); diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 8b4882c96..0d1af81a3 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -19,15 +19,15 @@ limitations under the License. /** * This is an internal module. See {@link createNewMatrixCall} for the public API. - * @module webrtc/call */ +import { v4 as uuidv4 } from "uuid"; 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 { EventType } from '../@types/event'; +import { IContent, MatrixEvent } from '../models/event'; +import { EventType, ToDeviceMessageId } from '../@types/event'; import { RoomMember } from '../models/room-member'; import { randomString } from '../randomstring'; import { @@ -53,29 +53,19 @@ import { GroupCallUnknownDeviceError } from './groupCall'; import { IScreensharingOpts } from "./mediaHandler"; import { MatrixError } from "../http-api"; -// events: hangup, error(err), replaced(call), state(state, oldState) - -/** - * Fires whenever an error occurs when call.js encounters an issue with setting up the call. - *

- * The error given will have a code equal to either `MatrixCall.ERR_LOCAL_OFFER_FAILED` or - * `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client - * fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access - * to their audio/video hardware. - * - * @event module:webrtc/call~MatrixCall#"error" - * @param {Error} err The error raised by MatrixCall. - * @example - * matrixCall.on("error", function(err){ - * console.error(err.code, err); - * }); - */ - interface CallOpts { + // The room ID for this call. roomId?: string; invitee?: string; + // The Matrix Client instance to send events to. client: MatrixClient; + /** + * Whether relay through TURN should be forced. + * @deprecated use opts.forceTURN when creating the matrix client + * since it's only possible to set this option on outbound calls. + */ forceTURN?: boolean; + // A list of TURN servers. turnServers?: Array; opponentDeviceId?: string; opponentSessionId?: string; @@ -263,7 +253,11 @@ const VOIP_PROTO_VERSION = "1"; const FALLBACK_ICE_SERVER = 'stun:turn.matrix.org'; /** The length of time a call can be ringing for. */ -const CALL_TIMEOUT_MS = 60000; +const CALL_TIMEOUT_MS = 60 * 1000; // ms +/** The time after which we increment callLength */ +const CALL_LENGTH_INTERVAL = 1000; // ms +/** The time after which we end the call, if ICE got disconnected */ +const ICE_DISCONNECTED_TIMEOUT = 30 * 1000; // ms export class CallError extends Error { public readonly code: string; @@ -319,22 +313,10 @@ function getTransceiverKey(purpose: SDPStreamMetadataPurpose, kind: TransceiverK return purpose + ':' + kind; } -/** - * Construct a new Matrix Call. - * @constructor - * @param {Object} opts Config options. - * @param {string} opts.roomId The room ID for this call. - * @param {Object} opts.webRtc The WebRTC globals from the browser. - * @param {boolean} opts.forceTURN whether relay through TURN should be forced. - * @param {Object} opts.URL The URL global. - * @param {Array} opts.turnServers Optional. A list of TURN servers. - * @param {MatrixClient} opts.client The Matrix Client instance to send events to. - */ export class MatrixCall extends TypedEventEmitter { public roomId?: string; public callId: string; public invitee?: string; - public state = CallState.Fledgling; public hangupParty?: CallParty; public hangupReason?: string; public direction?: CallDirection; @@ -346,6 +328,7 @@ export class MatrixCall extends TypedEventEmitter; @@ -397,13 +380,17 @@ export class MatrixCall extends TypedEventEmitter; - private callLength = 0; + private callStartTime?: number; private opponentDeviceId?: string; private opponentDeviceInfo?: DeviceInfo; private opponentSessionId?: string; public groupCallId?: string; + /** + * Construct a new Matrix Call. + * @param opts - Config options. + */ public constructor(opts: CallOpts) { super(); @@ -449,8 +436,8 @@ export class MatrixCall extends TypedEventEmitter} CallFeeds + * @returns CallFeeds */ public getFeeds(): Array { return this.feeds; @@ -554,7 +551,7 @@ export class MatrixCall extends TypedEventEmitter} local CallFeeds + * @returns local CallFeeds */ public getLocalFeeds(): Array { return this.feeds.filter((feed) => feed.isLocal()); @@ -562,7 +559,7 @@ export class MatrixCall extends TypedEventEmitter} remote CallFeeds + * @returns remote CallFeeds */ public getRemoteFeeds(): Array { return this.feeds.filter((feed) => !feed.isLocal()); @@ -594,7 +591,7 @@ export class MatrixCall extends TypedEventEmitter !feed.isLocal()); @@ -646,6 +643,7 @@ export class MatrixCall extends TypedEventEmitter callFeed.stream.id === feed.stream.id)) { @@ -807,7 +806,7 @@ export class MatrixCall extends TypedEventEmitter { const invite = event.getContent(); @@ -928,7 +927,7 @@ export class MatrixCall extends TypedEventEmitter { // Skip if there is nothing to do @@ -1220,9 +1219,9 @@ export class MatrixCall extends TypedEventEmitter { @@ -1358,7 +1357,7 @@ export class MatrixCall extends TypedEventEmitterall of the tracks need to be muted * for this to return true. This means if there are no video tracks, this will * return true. - * @return {Boolean} True if the local preview video is muted, else false + * @returns True if the local preview video is muted, else false * (including if the call is not set up yet). */ public isLocalVideoMuted(): boolean { @@ -1367,7 +1366,7 @@ export class MatrixCall extends TypedEventEmitter { @@ -1392,7 +1391,7 @@ export class MatrixCall extends TypedEventEmitterall of the tracks need to be muted * for this to return true. This means if there are no audio tracks, this will * return true. - * @return {Boolean} True if the mic is muted, else false (including if the call + * @returns True if the mic is muted, else false (including if the call * is not set up yet). */ public isMicrophoneMuted(): boolean { @@ -1446,7 +1445,7 @@ export class MatrixCall extends TypedEventEmitter { @@ -1665,7 +1664,6 @@ export class MatrixCall extends TypedEventEmitter { if (event.candidate) { @@ -1739,7 +1737,6 @@ export class MatrixCall extends TypedEventEmitter { const content = event.getContent(); @@ -1762,7 +1759,7 @@ export class MatrixCall extends TypedEventEmitter { this.inviteTimeout = undefined; if (this.state === CallState.InviteSent) { @@ -2088,13 +2085,14 @@ export class MatrixCall extends TypedEventEmitter { - this.callLength++; - this.emit(CallEvent.LengthChanged, this.callLength); - }, 1000); + this.emit(CallEvent.LengthChanged, Math.round((Date.now() - this.callStartTime!) / 1000)); + }, CALL_LENGTH_INTERVAL); } } else if (this.peerConn?.iceConnectionState == 'failed') { // Firefox for Android does not yet have support for restartIce() @@ -2111,8 +2109,8 @@ export class MatrixCall extends TypedEventEmitter { logger.info(`Hanging up call ${this.callId} (ICE disconnected for too long)`); this.hangup(CallErrorCode.IceFailed, false); - }, 30 * 1000); - this.setState(CallState.Connecting); + }, ICE_DISCONNECTED_TIMEOUT); + this.state = CallState.Connecting; } // In PTT mode, override feed status to muted when we lose connection to @@ -2244,17 +2242,8 @@ export class MatrixCall extends TypedEventEmitter { const realContent = Object.assign({}, content, { @@ -2272,6 +2261,7 @@ export class MatrixCall extends TypedEventEmitter void; }; diff --git a/src/webrtc/callFeed.ts b/src/webrtc/callFeed.ts index bc1c34450..3ed84a586 100644 --- a/src/webrtc/callFeed.ts +++ b/src/webrtc/callFeed.ts @@ -20,6 +20,7 @@ import { MatrixClient } from "../client"; import { RoomMember } from "../models/room-member"; import { logger } from "../logger"; import { TypedEventEmitter } from "../models/typed-event-emitter"; +import { CallEvent, CallState, MatrixCall } from "./call"; const POLLING_INTERVAL = 200; // ms export const SPEAKING_THRESHOLD = -60; // dB @@ -40,6 +41,10 @@ export interface ICallFeedOpts { * Whether or not the remote SDPStreamMetadata says video is muted */ videoMuted: boolean; + /** + * The MatrixCall which is the source of this CallFeed + */ + call?: MatrixCall; } export enum CallFeedEvent { @@ -47,6 +52,7 @@ export enum CallFeedEvent { MuteStateChanged = "mute_state_changed", LocalVolumeChanged = "local_volume_changed", VolumeChanged = "volume_changed", + ConnectedChanged = "connected_changed", Speaking = "speaking", Disposed = "disposed", } @@ -56,6 +62,7 @@ type EventHandlerMap = { [CallFeedEvent.MuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void; [CallFeedEvent.LocalVolumeChanged]: (localVolume: number) => void; [CallFeedEvent.VolumeChanged]: (volume: number) => void; + [CallFeedEvent.ConnectedChanged]: (connected: boolean) => void; [CallFeedEvent.Speaking]: (speaking: boolean) => void; [CallFeedEvent.Disposed]: () => void; }; @@ -69,6 +76,7 @@ export class CallFeed extends TypedEventEmitter public speakingVolumeSamples: number[]; private client: MatrixClient; + private call?: MatrixCall; private roomId?: string; private audioMuted: boolean; private videoMuted: boolean; @@ -81,11 +89,13 @@ export class CallFeed extends TypedEventEmitter private speaking = false; private volumeLooperTimeout?: ReturnType; private _disposed = false; + private _connected = false; public constructor(opts: ICallFeedOpts) { super(); this.client = opts.client; + this.call = opts.call; this.roomId = opts.roomId; this.userId = opts.userId; this.deviceId = opts.deviceId; @@ -101,6 +111,21 @@ export class CallFeed extends TypedEventEmitter if (this.hasAudioTrack) { this.initVolumeMeasuring(); } + + if (opts.call) { + opts.call.addListener(CallEvent.State, this.onCallState); + this.onCallState(opts.call.state); + } + } + + public get connected(): boolean { + // Local feeds are always considered connected + return this.isLocal() || this._connected; + } + + private set connected(connected: boolean) { + this._connected = connected; + this.emit(CallFeedEvent.ConnectedChanged, this.connected); } private get hasAudioTrack(): boolean { @@ -145,6 +170,14 @@ export class CallFeed extends TypedEventEmitter this.emit(CallFeedEvent.NewStream, this.stream); }; + private onCallState = (state: CallState): void => { + if (state === CallState.Connected) { + this.connected = true; + } else if (state === CallState.Connecting) { + this.connected = false; + } + }; + /** * Returns callRoom member * @returns member of the callRoom @@ -156,7 +189,7 @@ export class CallFeed extends TypedEventEmitter /** * Returns true if CallFeed is local, otherwise returns false - * @returns {boolean} is local? + * @returns is local? */ public isLocal(): boolean { return this.userId === this.client.getUserId() @@ -166,7 +199,7 @@ export class CallFeed extends TypedEventEmitter /** * Returns true if audio is muted or if there are no audio * tracks, otherwise returns false - * @returns {boolean} is audio muted? + * @returns is audio muted? */ public isAudioMuted(): boolean { return this.stream.getAudioTracks().length === 0 || this.audioMuted; @@ -175,7 +208,7 @@ export class CallFeed extends TypedEventEmitter /** * Returns true video is muted or if there are no video * tracks, otherwise returns false - * @returns {boolean} is video muted? + * @returns is video muted? */ public isVideoMuted(): boolean { // We assume only one video track @@ -191,7 +224,7 @@ export class CallFeed extends TypedEventEmitter * The stream will be different and new stream as remore parties are * concerned, but this can be used for convenience locally to set up * volume listeners automatically on the new stream etc. - * @param newStream new stream with which to replace the current one + * @param newStream - new stream with which to replace the current one */ public setNewStream(newStream: MediaStream): void { this.updateStream(this.stream, newStream); @@ -200,8 +233,8 @@ export class CallFeed extends TypedEventEmitter /** * Set one or both of feed's internal audio and video video mute state * Either value may be null to leave it as-is - * @param audioMuted is the feed's audio muted? - * @param videoMuted is the feed's video muted? + * @param audioMuted - is the feed's audio muted? + * @param videoMuted - is the feed's video muted? */ public setAudioVideoMuted(audioMuted: boolean | null, videoMuted: boolean | null): void { if (audioMuted !== null) { @@ -216,7 +249,7 @@ export class CallFeed extends TypedEventEmitter /** * Starts emitting volume_changed events where the emitter value is in decibels - * @param enabled emit volume changes + * @param enabled - emit volume changes */ public measureVolumeActivity(enabled: boolean): void { if (enabled) { @@ -297,6 +330,7 @@ export class CallFeed extends TypedEventEmitter public dispose(): void { clearTimeout(this.volumeLooperTimeout); this.stream?.removeEventListener("addtrack", this.onAddTrack); + this.call?.removeListener(CallEvent.State, this.onCallState); if (this.audioContext) { this.audioContext = undefined; this.analyser = undefined; diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index 7713d6de9..58281b0e9 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -63,6 +63,21 @@ export type GroupCallEventHandlerMap = { ) => void; [GroupCallEvent.LocalMuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void; [GroupCallEvent.ParticipantsChanged]: (participants: Map>) => void; + /** + * Fires whenever an error occurs when call.js encounters an issue with setting up the call. + *

+ * The error given will have a code equal to either `MatrixCall.ERR_LOCAL_OFFER_FAILED` or + * `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client + * fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access + * to their audio/video hardware. + * @param err - The error raised by MatrixCall. + * @example + * ``` + * matrixCall.on("error", function(err){ + * console.error(err.code, err); + * }); + * ``` + */ [GroupCallEvent.Error]: (error: GroupCallError) => void; }; @@ -106,9 +121,17 @@ export interface IGroupCallDataChannelOptions { protocol: string; } +export interface IGroupCallRoomState { + "m.intent": GroupCallIntent; + "m.type": GroupCallType; + "io.element.ptt"?: boolean; + // TODO: Specify data-channels + "dataChannelsEnabled"?: boolean; + "dataChannelOptions"?: IGroupCallDataChannelOptions; +} + export interface IGroupCallRoomMemberFeed { purpose: SDPStreamMetadataPurpose; - // TODO: Sources for adaptive bitrate } export interface IGroupCallRoomMemberDevice { @@ -168,11 +191,11 @@ export class GroupCall extends TypedEventEmitter< public localCallFeed?: CallFeed; public localScreenshareFeed?: CallFeed; public localDesktopCapturerSourceId?: string; - public readonly calls = new Map>(); public readonly userMediaFeeds: CallFeed[] = []; public readonly screenshareFeeds: CallFeed[] = []; public groupCallId: string; + private readonly calls = new Map>(); // RoomMember -> device ID -> MatrixCall private callHandlers = new Map>(); // User ID -> device ID -> handlers private activeSpeakerLoopInterval?: ReturnType; private retryCallLoopInterval?: ReturnType; @@ -213,17 +236,19 @@ export class GroupCall extends TypedEventEmitter< this.client.groupCallEventHandler!.groupCalls.set(this.room.roomId, this); this.client.emit(GroupCallEventHandlerEvent.Outgoing, this); + const groupCallState: IGroupCallRoomState = { + "m.intent": this.intent, + "m.type": this.type, + "io.element.ptt": this.isPtt, + // TODO: Specify data-channels better + "dataChannelsEnabled": this.dataChannelsEnabled, + "dataChannelOptions": this.dataChannelsEnabled ? this.dataChannelOptions : undefined, + }; + await this.client.sendStateEvent( this.room.roomId, EventType.GroupCallPrefix, - { - "m.intent": this.intent, - "m.type": this.type, - "io.element.ptt": this.isPtt, - // TODO: Specify datachannels - "dataChannelsEnabled": this.dataChannelsEnabled, - "dataChannelOptions": this.dataChannelOptions, - }, + groupCallState, this.groupCallId, ); @@ -285,9 +310,24 @@ export class GroupCall extends TypedEventEmitter< this._creationTs = value; } + private _enteredViaAnotherSession = false; + + /** + * Whether the local device has entered this call via another session, such + * as a widget. + */ + public get enteredViaAnotherSession(): boolean { + return this._enteredViaAnotherSession; + } + + public set enteredViaAnotherSession(value: boolean) { + this._enteredViaAnotherSession = value; + this.updateParticipants(); + } + /** * Executes the given callback on all calls in this group call. - * @param f The callback. + * @param f - The callback. */ public forEachCall(f: (call: MatrixCall) => void): void { for (const deviceMap of this.calls.values()) { @@ -497,8 +537,8 @@ export class GroupCall extends TypedEventEmitter< /** * Sets the mute state of the local participants's microphone. - * @param {boolean} muted Whether to mute the microphone - * @returns {Promise} Whether muting/unmuting was successful + * @param muted - Whether to mute the microphone + * @returns Whether muting/unmuting was successful */ public async setMicrophoneMuted(muted: boolean): Promise { // hasAudioDevice can block indefinitely if the window has lost focus, @@ -560,8 +600,8 @@ export class GroupCall extends TypedEventEmitter< /** * Sets the mute state of the local participants's video. - * @param {boolean} muted Whether to mute the video - * @returns {Promise} Whether muting/unmuting was successful + * @param muted - Whether to mute the video + * @returns Whether muting/unmuting was successful */ public async setLocalVideoMuted(muted: boolean): Promise { // hasAudioDevice can block indefinitely if the window has lost focus, @@ -718,8 +758,8 @@ export class GroupCall extends TypedEventEmitter< /** * Determines whether a given participant expects us to call them (versus * them calling us). - * @param userId The participant's user ID. - * @param deviceId The participant's device ID. + * @param userId - The participant's user ID. + * @param deviceId - The participant's device ID. * @returns Whether we need to place an outgoing call to the participant. */ private wantsOutgoingCall(userId: string, deviceId: string): boolean { @@ -1170,7 +1210,7 @@ export class GroupCall extends TypedEventEmitter< const participants = new Map>(); const now = Date.now(); - const entered = this.state === GroupCallState.Entered; + const entered = this.state === GroupCallState.Entered || this.enteredViaAnotherSession; let nextExpiration = Infinity; for (const e of this.getMemberStateEvents()) { @@ -1234,9 +1274,9 @@ export class GroupCall extends TypedEventEmitter< /** * Updates the local user's member state with the devices returned by the given function. - * @param fn A function from the current devices to the new devices. If it + * @param fn - A function from the current devices to the new devices. If it * returns null, the update will be skipped. - * @param keepAlive Whether the request should outlive the window. + * @param keepAlive - Whether the request should outlive the window. */ private async updateDevices( fn: (devices: IGroupCallRoomMemberDevice[]) => IGroupCallRoomMemberDevice[] | null, @@ -1344,8 +1384,11 @@ export class GroupCall extends TypedEventEmitter< await this.updateDevices(devices => { const newDevices = devices.filter(d => { const device = deviceMap.get(d.device_id); - return device?.last_seen_ts !== undefined - && !(d.device_id === this.client.getDeviceId()! && this.state !== GroupCallState.Entered); + return device?.last_seen_ts !== undefined && !( + d.device_id === this.client.getDeviceId()! + && this.state !== GroupCallState.Entered + && !this.enteredViaAnotherSession + ); }); // Skip the update if the devices are unchanged diff --git a/src/webrtc/mediaHandler.ts b/src/webrtc/mediaHandler.ts index c7c84876b..46943ae38 100644 --- a/src/webrtc/mediaHandler.ts +++ b/src/webrtc/mediaHandler.ts @@ -67,7 +67,7 @@ export class MediaHandler extends TypedEventEmitter< /** * Set an audio input device to use for MatrixCalls - * @param {string} deviceId the identifier for the device + * @param deviceId - the identifier for the device * undefined treated as unset */ public async setAudioInput(deviceId: string): Promise { @@ -81,7 +81,7 @@ export class MediaHandler extends TypedEventEmitter< /** * Set audio settings for MatrixCalls - * @param {AudioSettings} opts audio options to set + * @param opts - audio options to set */ public async setAudioSettings(opts: AudioSettings): Promise { logger.info("Setting audio settings to", opts); @@ -92,7 +92,7 @@ export class MediaHandler extends TypedEventEmitter< /** * Set a video input device to use for MatrixCalls - * @param {string} deviceId the identifier for the device + * @param deviceId - the identifier for the device * undefined treated as unset */ public async setVideoInput(deviceId: string): Promise { @@ -106,8 +106,8 @@ export class MediaHandler extends TypedEventEmitter< /** * Set media input devices to use for MatrixCalls - * @param {string} audioInput the identifier for the audio device - * @param {string} videoInput the identifier for the video device + * @param audioInput - the identifier for the audio device + * @param videoInput - the identifier for the video device * undefined treated as unset */ public async setMediaInputs(audioInput: string, videoInput: string): Promise { @@ -191,10 +191,10 @@ export class MediaHandler extends TypedEventEmitter< } /** - * @param audio should have an audio track - * @param video should have a video track - * @param reusable is allowed to be reused by the MediaHandler - * @returns {MediaStream} based on passed parameters + * @param audio - should have an audio track + * @param video - should have a video track + * @param reusable - is allowed to be reused by the MediaHandler + * @returns based on passed parameters */ public async getUserMediaStream(audio: boolean, video: boolean, reusable = true): Promise { const shouldRequestAudio = audio && await this.hasAudioDevice(); @@ -296,9 +296,9 @@ export class MediaHandler extends TypedEventEmitter< } /** - * @param desktopCapturerSourceId sourceId for Electron DesktopCapturer - * @param reusable is allowed to be reused by the MediaHandler - * @returns {MediaStream} based on passed parameters + * @param desktopCapturerSourceId - sourceId for Electron DesktopCapturer + * @param reusable - is allowed to be reused by the MediaHandler + * @returns based on passed parameters */ public async getScreensharingStream(opts: IScreensharingOpts = {}, reusable = true): Promise { let stream: MediaStream; diff --git a/tsconfig.json b/tsconfig.json index 9644f5c5f..f34858ce9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,6 @@ "esModuleInterop": true, "module": "commonjs", "moduleResolution": "node", - "noImplicitAny": false, "noUnusedLocals": true, "noEmit": true, "declaration": true, diff --git a/yarn.lock b/yarn.lock index 67c5baeb4..9a2f14794 100644 --- a/yarn.lock +++ b/yarn.lock @@ -58,26 +58,31 @@ dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.0", "@babel/compat-data@^7.20.1": +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1": version "7.20.1" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.1.tgz#f2e6ef7790d8c8dbf03d379502dcc246dcce0b30" integrity sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ== +"@babel/compat-data@^7.20.0": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" + integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== + "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.5": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.2.tgz#8dc9b1620a673f92d3624bd926dc49a52cf25b92" - integrity sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g== + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" + integrity sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.2" + "@babel/generator" "^7.20.5" "@babel/helper-compilation-targets" "^7.20.0" "@babel/helper-module-transforms" "^7.20.2" - "@babel/helpers" "^7.20.1" - "@babel/parser" "^7.20.2" + "@babel/helpers" "^7.20.5" + "@babel/parser" "^7.20.5" "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.2" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -109,7 +114,16 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" -"@babel/generator@^7.20.1", "@babel/generator@^7.20.2", "@babel/generator@^7.7.2": +"@babel/generator@^7.20.1", "@babel/generator@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" + integrity sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA== + dependencies: + "@babel/types" "^7.20.5" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/generator@^7.7.2": version "7.20.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.4.tgz#4d9f8f0c30be75fd90a0562099a26e5839602ab8" integrity sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA== @@ -310,14 +324,14 @@ "@babel/traverse" "^7.19.0" "@babel/types" "^7.19.0" -"@babel/helpers@^7.20.1": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.1.tgz#2ab7a0fcb0a03b5bf76629196ed63c2d7311f4c9" - integrity sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg== +"@babel/helpers@^7.20.5": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" + integrity sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w== dependencies: "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.0" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" "@babel/highlight@^7.18.6": version "7.18.6" @@ -328,11 +342,16 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.2.3", "@babel/parser@^7.20.1", "@babel/parser@^7.20.2": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.2.3": version "7.20.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2" integrity sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg== +"@babel/parser@^7.18.10", "@babel/parser@^7.20.1", "@babel/parser@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" + integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -995,11 +1014,11 @@ source-map-support "^0.5.16" "@babel/runtime@^7.12.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" - integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg== + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" + integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== dependencies: - regenerator-runtime "^0.13.10" + regenerator-runtime "^0.13.11" "@babel/template@^7.18.10", "@babel/template@^7.3.3": version "7.18.10" @@ -1010,7 +1029,7 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@^7.1.6", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.1.6", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.7.2": version "7.20.1" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8" integrity sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA== @@ -1026,7 +1045,23 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" + integrity sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.5" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.20.5" + "@babel/types" "^7.20.5" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.2.tgz#67ac09266606190f496322dbaff360fdaa5e7842" integrity sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog== @@ -1035,6 +1070,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" + integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1049,6 +1093,22 @@ uuid "8.3.2" xml "1.0.1" +"@es-joy/jsdoccomment@~0.36.1": + version "0.36.1" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz#c37db40da36e4b848da5fd427a74bae3b004a30f" + integrity sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg== + dependencies: + comment-parser "1.3.1" + esquery "^1.4.0" + jsdoc-type-pratt-parser "~3.1.0" + +"@eslint-community/eslint-utils@^4.1.0": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.1.2.tgz#14ca568ddaa291dd19a4a54498badc18c6cfab78" + integrity sha512-7qELuQWWjVDdVsFQ5+beUl+KPczrEDA7S3zM4QUd/bJl7oXgsmpXaEVqrRTnOBqenOV4rWf2kVZk2Ot085zPWA== + dependencies: + eslint-visitor-keys "^3.3.0" + "@eslint/eslintrc@^1.3.3": version "1.3.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95" @@ -1372,9 +1432,24 @@ dependencies: lodash "^4.17.21" -"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz": - version "3.2.13" - resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz#0109fde93bcc61def851f79826c9384c073b5175" +"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": + version "3.2.14" + resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984" + +"@microsoft/tsdoc-config@0.16.2": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz#b786bb4ead00d54f53839a458ce626c8548d3adf" + integrity sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw== + dependencies: + "@microsoft/tsdoc" "0.14.2" + ajv "~6.12.6" + jju "~1.4.0" + resolve "~1.19.0" + +"@microsoft/tsdoc@0.14.2": + version "0.14.2" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz#c3ec604a0b54b9a9b87e9735dfc59e1a5da6a5fb" + integrity sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" @@ -1708,6 +1783,11 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== +"@types/uuid@7": + version "7.0.5" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-7.0.5.tgz#b1d2f772142a301538fae9bdf9cf15b9f2573a29" + integrity sha512-hKB88y3YHL8oPOs/CNlaXtjWn93+Bs48sDQR37ZUqG2tLeCS7EA1cmnkKsuQsub9OKEB/y/Rw9zqJqqNSbqVlQ== + "@types/webidl-conversions@*": version "7.0.0" resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz#2b8e60e33906459219aa587e9d1a612ae994cfe7" @@ -1725,14 +1805,14 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.6.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.43.0.tgz#4a5248eb31b454715ddfbf8cfbf497529a0a78bc" - integrity sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA== +"@typescript-eslint/eslint-plugin@^5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.0.tgz#ffa505cf961d4844d38cfa19dcec4973a6039e41" + integrity sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA== dependencies: - "@typescript-eslint/scope-manager" "5.43.0" - "@typescript-eslint/type-utils" "5.43.0" - "@typescript-eslint/utils" "5.43.0" + "@typescript-eslint/scope-manager" "5.45.0" + "@typescript-eslint/type-utils" "5.45.0" + "@typescript-eslint/utils" "5.45.0" debug "^4.3.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" @@ -1740,72 +1820,72 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.6.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.43.0.tgz#9c86581234b88f2ba406f0b99a274a91c11630fd" - integrity sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug== +"@typescript-eslint/parser@^5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.45.0.tgz#b18a5f6b3cf1c2b3e399e9d2df4be40d6b0ddd0e" + integrity sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ== dependencies: - "@typescript-eslint/scope-manager" "5.43.0" - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/typescript-estree" "5.43.0" + "@typescript-eslint/scope-manager" "5.45.0" + "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/typescript-estree" "5.45.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.43.0.tgz#566e46303392014d5d163704724872e1f2dd3c15" - integrity sha512-XNWnGaqAtTJsUiZaoiGIrdJYHsUOd3BZ3Qj5zKp9w6km6HsrjPk/TGZv0qMTWyWj0+1QOqpHQ2gZOLXaGA9Ekw== +"@typescript-eslint/scope-manager@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz#7a4ac1bfa9544bff3f620ab85947945938319a96" + integrity sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw== dependencies: - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/visitor-keys" "5.43.0" + "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/visitor-keys" "5.45.0" -"@typescript-eslint/type-utils@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.43.0.tgz#91110fb827df5161209ecca06f70d19a96030be6" - integrity sha512-K21f+KY2/VvYggLf5Pk4tgBOPs2otTaIHy2zjclo7UZGLyFH86VfUOm5iq+OtDtxq/Zwu2I3ujDBykVW4Xtmtg== +"@typescript-eslint/type-utils@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.45.0.tgz#aefbc954c40878fcebeabfb77d20d84a3da3a8b2" + integrity sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q== dependencies: - "@typescript-eslint/typescript-estree" "5.43.0" - "@typescript-eslint/utils" "5.43.0" + "@typescript-eslint/typescript-estree" "5.45.0" + "@typescript-eslint/utils" "5.45.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.43.0.tgz#e4ddd7846fcbc074325293515fa98e844d8d2578" - integrity sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg== +"@typescript-eslint/types@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5" + integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA== -"@typescript-eslint/typescript-estree@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz#b6883e58ba236a602c334be116bfc00b58b3b9f2" - integrity sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg== +"@typescript-eslint/typescript-estree@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d" + integrity sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ== dependencies: - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/visitor-keys" "5.43.0" + "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/visitor-keys" "5.45.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.43.0.tgz#00fdeea07811dbdf68774a6f6eacfee17fcc669f" - integrity sha512-8nVpA6yX0sCjf7v/NDfeaOlyaIIqL7OaIGOWSPFqUKK59Gnumd3Wa+2l8oAaYO2lk0sO+SbWFWRSvhu8gLGv4A== +"@typescript-eslint/utils@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.45.0.tgz#9cca2996eee1b8615485a6918a5c763629c7acf5" + integrity sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.43.0" - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/typescript-estree" "5.43.0" + "@typescript-eslint/scope-manager" "5.45.0" + "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/typescript-estree" "5.45.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.43.0.tgz#cbbdadfdfea385310a20a962afda728ea106befa" - integrity sha512-icl1jNH/d18OVHLfcwdL3bWUKsBeIiKYTGxMJCoGe7xFht+E4QgzOqoWYrU8XSLJWhVw8nTacbm03v23J/hFTg== +"@typescript-eslint/visitor-keys@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528" + integrity sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg== dependencies: - "@typescript-eslint/types" "5.43.0" + "@typescript-eslint/types" "5.45.0" eslint-visitor-keys "^3.3.0" JSONStream@^1.0.3: @@ -1892,7 +1972,7 @@ agent-base@6: dependencies: debug "4" -ajv@^6.10.0, ajv@^6.12.4: +ajv@^6.10.0, ajv@^6.12.4, ajv@~6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2500,9 +2580,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001400: - version "1.0.30001431" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz#e7c59bd1bc518fae03a4656be442ce6c4887a795" - integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ== + version "1.0.30001435" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001435.tgz#502c93dbd2f493bee73a408fe98e98fb1dad10b2" + integrity sha512-kdCkUTjR+v4YAJelyiDTqiu82BDr4W4CP5sgTA0ZBmqn30XfS2ZghPLMowik9TPhS+psWJiUNxsqLyurDbmutA== center-align@^0.1.1: version "0.1.3" @@ -2556,11 +2636,16 @@ chokidar@^3.4.0: optionalDependencies: fsevents "~2.3.2" -ci-info@^3.2.0, ci-info@^3.6.1: +ci-info@^3.2.0: version "3.6.1" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.6.1.tgz#7594f1c95cb7fdfddee7af95a13af7dbc67afdcf" integrity sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w== +ci-info@^3.6.1: + version "3.7.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef" + integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog== + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -2696,6 +2781,11 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +comment-parser@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b" + integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -3277,29 +3367,50 @@ eslint-plugin-import@^2.26.0: resolve "^1.22.0" tsconfig-paths "^3.14.1" +eslint-plugin-jsdoc@^39.6.4: + version "39.6.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.6.4.tgz#b940aebd3eea26884a0d341785d2dc3aba6a38a7" + integrity sha512-fskvdLCfwmPjHb6e+xNGDtGgbF8X7cDwMtVLAP2WwSf9Htrx68OAx31BESBM1FAwsN2HTQyYQq7m4aW4Q4Nlag== + dependencies: + "@es-joy/jsdoccomment" "~0.36.1" + comment-parser "1.3.1" + debug "^4.3.4" + escape-string-regexp "^4.0.0" + esquery "^1.4.0" + semver "^7.3.8" + spdx-expression-parse "^3.0.1" + eslint-plugin-matrix-org@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.8.0.tgz#daa1396900a8cb1c1d88f1a370e45fc32482cd9e" integrity sha512-/Poz/F8lXYDsmQa29iPSt+kO+Jn7ArvRdq10g0CCk8wbRS0sb2zb6fvd9xL1BgR5UDQL771V0l8X32etvY5yKA== +eslint-plugin-tsdoc@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.17.tgz#27789495bbd8778abbf92db1707fec2ed3dfe281" + integrity sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA== + dependencies: + "@microsoft/tsdoc" "0.14.2" + "@microsoft/tsdoc-config" "0.16.2" + eslint-plugin-unicorn@^45.0.0: - version "45.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-45.0.0.tgz#a6650ff3000dc1a87cc2f6ac3a11edcde61712e2" - integrity sha512-iP8cMRxXKHonKioOhnCoCcqVhoqhAp6rB+nsoLjXFDxTHz3btWMAp8xwzjHA0B1K6YV/U/Yvqn1bUXZt8sJPuQ== + version "45.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-45.0.1.tgz#2307f4620502fd955c819733ce1276bed705b736" + integrity sha512-tLnIw5oDJJc3ILYtlKtqOxPP64FZLTkZkgeuoN6e7x6zw+rhBjOxyvq2c7577LGxXuIhBYrwisZuKNqOOHp3BA== dependencies: "@babel/helper-validator-identifier" "^7.19.1" + "@eslint-community/eslint-utils" "^4.1.0" ci-info "^3.6.1" clean-regexp "^1.0.0" - eslint-utils "^3.0.0" esquery "^1.4.0" indent-string "^4.0.0" is-builtin-module "^3.2.0" - jsesc "3.0.2" + jsesc "^3.0.2" lodash "^4.17.21" pluralize "^8.0.0" read-pkg-up "^7.0.1" regexp-tree "^0.1.24" - regjsparser "0.9.1" + regjsparser "^0.9.1" safe-regex "^2.1.1" semver "^7.3.8" strip-indent "^3.0.0" @@ -3520,9 +3631,9 @@ ext@^1.1.2: type "^2.7.2" fake-indexeddb@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-4.0.0.tgz#1dfb2023a3be175e35a6d84975218b432041934d" - integrity sha512-oCfWSJ/qvQn1XPZ8SHX6kY3zr1t+bN7faZ/lltGY0SBGhFOPXnWf0+pbO/MOAgfMx6khC2gK3S/bvAgQpuQHDQ== + version "4.0.1" + resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-4.0.1.tgz#09bb2468e21d0832b2177e894765fb109edac8fb" + integrity sha512-hFRyPmvEZILYgdcLBxVdHLik4Tj3gDTu/g7s9ZDOiU3sTNiGx+vEu1ri/AMsFJUZ/1sdRbAVrEcKndh3sViBcA== dependencies: realistic-structured-clone "^3.0.0" @@ -3963,9 +4074,9 @@ ieee754@^1.1.4: integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + version "5.2.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" + integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" @@ -4095,7 +4206,7 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.10.0, is-core-module@^2.8.1, is-core-module@^2.9.0: +is-core-module@^2.1.0, is-core-module@^2.10.0, is-core-module@^2.8.1, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== @@ -4748,6 +4859,11 @@ jest@^29.0.0: import-local "^3.0.2" jest-cli "^29.3.1" +jju@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" + integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== + js-sdsl@^4.1.4: version "4.1.5" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.5.tgz#1ff1645e6b4d1b028cd3f862db88c9d887f26e2a" @@ -4778,6 +4894,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsdoc-type-pratt-parser@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz#a4a56bdc6e82e5865ffd9febc5b1a227ff28e67e" + integrity sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw== + jsdom@^20.0.0: version "20.0.2" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.2.tgz#65ccbed81d5e877c433f353c58bb91ff374127db" @@ -4810,16 +4931,16 @@ jsdom@^20.0.0: ws "^8.9.0" xml-name-validator "^4.0.0" -jsesc@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== - jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" @@ -5507,7 +5628,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.7: +path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -5957,10 +6078,10 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.10: - version "0.13.10" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" - integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== regenerator-transform@^0.15.0: version "0.15.0" @@ -6005,7 +6126,7 @@ regjsgen@^0.7.1: resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.7.1.tgz#ee5ef30e18d3f09b7c369b76e7c2373ed25546f6" integrity sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA== -regjsparser@0.9.1, regjsparser@^0.9.1: +regjsparser@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== @@ -6058,6 +6179,14 @@ resolve@^1.1.4, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.17. path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@~1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + dependencies: + is-core-module "^2.1.0" + path-parse "^1.0.6" + retry@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -6283,7 +6412,7 @@ spdx-exceptions@^2.1.0: resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== -spdx-expression-parse@^3.0.0: +spdx-expression-parse@^3.0.0, spdx-expression-parse@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== @@ -6496,9 +6625,9 @@ tapable@^2.2.0: integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== terser@^5.5.1: - version "5.15.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.1.tgz#8561af6e0fd6d839669c73b92bdd5777d870ed6c" - integrity sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw== + version "5.16.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.0.tgz#29362c6f5506e71545c73b069ccd199bb28f7f54" + integrity sha512-KjTV81QKStSfwbNiwlBXfcgMcOloyuRdb62/iLFPGBcVNF4EXjhdYBhYHmbJpiBrVxZhDvltE11j+LBQUxEEJg== dependencies: "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" @@ -6911,6 +7040,11 @@ util@~0.12.0: is-typed-array "^1.1.3" which-typed-array "^1.1.2" +uuid@7: + version "7.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" + integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== + uuid@8.3.2, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"