diff --git a/.github/workflows/docs-pr-netlify.yaml b/.github/workflows/docs-pr-netlify.yaml index bad72ef66c4..5891876fb0d 100644 --- a/.github/workflows/docs-pr-netlify.yaml +++ b/.github/workflows/docs-pr-netlify.yaml @@ -22,7 +22,7 @@ jobs: path: docs - name: 📤 Deploy to Netlify - uses: matrix-org/netlify-pr-preview@v1 + uses: matrix-org/netlify-pr-preview@v2 with: path: docs owner: ${{ github.event.workflow_run.head_repository.owner.login }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c84bbdabc70..c538f868e8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +Changes in [26.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.0.0) (2023-06-06) +================================================================================================== + +## 🚨 BREAKING CHANGES + * Ensure we do not add relations to the wrong timeline ([\#3427](https://github.com/matrix-org/matrix-js-sdk/pull/3427)). Fixes vector-im/element-web#25450 and vector-im/element-web#25494. + * Deprecate `QrCodeEvent`, `SasEvent` and `VerificationEvent` ([\#3386](https://github.com/matrix-org/matrix-js-sdk/pull/3386)). + +## 🦖 Deprecations + * Move crypto classes into a separate namespace ([\#3385](https://github.com/matrix-org/matrix-js-sdk/pull/3385)). + +## ✨ Features + * Mention deno support in the README ([\#3417](https://github.com/matrix-org/matrix-js-sdk/pull/3417)). Contributed by @sigmaSd. + * Mark room version 10 as safe ([\#3425](https://github.com/matrix-org/matrix-js-sdk/pull/3425)). + * Prioritise entirely supported flows for UIA ([\#3402](https://github.com/matrix-org/matrix-js-sdk/pull/3402)). + * Add methods to terminate idb worker ([\#3362](https://github.com/matrix-org/matrix-js-sdk/pull/3362)). + * Total summary count ([\#3351](https://github.com/matrix-org/matrix-js-sdk/pull/3351)). Contributed by @toger5. + * Audio concealment ([\#3349](https://github.com/matrix-org/matrix-js-sdk/pull/3349)). Contributed by @toger5. + +## 🐛 Bug Fixes + * Correctly accumulate sync summaries. ([\#3366](https://github.com/matrix-org/matrix-js-sdk/pull/3366)). Fixes vector-im/element-web#23345. + * Keep measuring a call feed's volume after a stream replacement ([\#3361](https://github.com/matrix-org/matrix-js-sdk/pull/3361)). Fixes vector-im/element-call#1051. + * Element-R: Avoid uploading a new fallback key at every `/sync` ([\#3338](https://github.com/matrix-org/matrix-js-sdk/pull/3338)). Fixes vector-im/element-web#25215. + * Accumulate receipts for the main thread and unthreaded separately ([\#3339](https://github.com/matrix-org/matrix-js-sdk/pull/3339)). Fixes vector-im/element-web#24629. + * Remove spec non-compliant extended glob format ([\#3423](https://github.com/matrix-org/matrix-js-sdk/pull/3423)). Fixes vector-im/element-web#25474. + * Fix bug where original event was inserted into timeline instead of the edit event ([\#3398](https://github.com/matrix-org/matrix-js-sdk/pull/3398)). Contributed by @andybalaam. + * Only add a local receipt if it's after an existing receipt ([\#3399](https://github.com/matrix-org/matrix-js-sdk/pull/3399)). Contributed by @andybalaam. + * Attempt a potential workaround for stuck notifs ([\#3384](https://github.com/matrix-org/matrix-js-sdk/pull/3384)). Fixes vector-im/element-web#25406. Contributed by @andybalaam. + * Fix verification bug with `pendingEventOrdering: "chronological"` ([\#3382](https://github.com/matrix-org/matrix-js-sdk/pull/3382)). + +Changes in [25.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.1.1) (2023-05-16) +================================================================================================== + +## 🐛 Bug Fixes + * Rebuild to fix packaging glitch in 25.1.0. Fixes #3363 + Changes in [25.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.1.0) (2023-05-09) ================================================================================================== diff --git a/README.md b/README.md index 71c8cba2ede..ca359d238fb 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ client.publicRooms(function (err, data) { See below for how to include libolm to enable end-to-end-encryption. Please check [the Node.js terminal app](examples/node) for a more complex example. +You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported. + To start the client: ```javascript diff --git a/package.json b/package.json index af1c9c71e1e..3836c0c064b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "25.1.0", + "version": "26.0.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=16.0.0" @@ -55,7 +55,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.7", + "@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.9", "another-json": "^0.2.0", "bs58": "^5.0.0", "content-type": "^1.0.4", @@ -101,16 +101,16 @@ "debug": "^4.3.4", "docdash": "^2.0.0", "domexception": "^4.0.0", - "eslint": "8.39.0", + "eslint": "8.40.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-typescript": "^3.5.1", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "^27.1.6", - "eslint-plugin-jsdoc": "^43.0.6", + "eslint-plugin-jsdoc": "^44.0.0", "eslint-plugin-matrix-org": "^1.0.0", "eslint-plugin-tsdoc": "^0.2.17", - "eslint-plugin-unicorn": "^46.0.0", + "eslint-plugin-unicorn": "^47.0.0", "exorcist": "^2.0.0", "fake-indexeddb": "^4.0.0", "fetch-mock-jest": "^1.5.1", @@ -120,14 +120,16 @@ "jest-mock": "^29.0.0", "matrix-mock-request": "^2.5.0", "prettier": "2.8.8", - "rimraf": "^4.0.0", + "rimraf": "^5.0.0", "terser": "^5.5.1", "ts-node": "^10.9.1", "tsify": "^5.0.2", "typedoc": "^0.24.0", + "typedoc-plugin-coverage": "^2.1.0", "typedoc-plugin-mdn-links": "^3.0.3", "typedoc-plugin-missing-exports": "^2.0.0", "typedoc-plugin-versions": "^0.2.3", + "typedoc-plugin-versions-cli": "^0.1.12", "typescript": "^5.0.0" }, "@casualbot/jest-sonar-reporter": { diff --git a/scripts/changelog_head.py b/scripts/changelog_head.py index c7c1c0aeaa5..dfe54b466d4 100755 --- a/scripts/changelog_head.py +++ b/scripts/changelog_head.py @@ -15,4 +15,4 @@ break found_first_header = True elif not re.match(r"^=+$", line) and len(line) > 0: - print line + print(line) diff --git a/spec/integ/cross-signing.spec.ts b/spec/integ/cross-signing.spec.ts new file mode 100644 index 00000000000..ba227d3f89c --- /dev/null +++ b/spec/integ/cross-signing.spec.ts @@ -0,0 +1,117 @@ +/* +Copyright 2023 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 fetchMock from "fetch-mock-jest"; +import "fake-indexeddb/auto"; +import { IDBFactory } from "fake-indexeddb"; + +import { CRYPTO_BACKENDS, InitCrypto } from "../test-utils/test-utils"; +import { createClient, MatrixClient, UIAuthCallback } from "../../src"; + +afterEach(() => { + // reset fake-indexeddb after each test, to make sure we don't leak connections + // cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state + // eslint-disable-next-line no-global-assign + indexedDB = new IDBFactory(); +}); + +const TEST_USER_ID = "@alice:localhost"; +const TEST_DEVICE_ID = "xzcvb"; + +/** + * Integration tests for cross-signing functionality. + * + * These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as + * to provide the most effective integration tests possible. + */ +describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => { + let aliceClient: MatrixClient; + + beforeEach(async () => { + // anything that we don't have a specific matcher for silently returns a 404 + fetchMock.catch(404); + fetchMock.config.warnOnFallback = false; + + const homeserverUrl = "https://alice-server.com"; + aliceClient = createClient({ + baseUrl: homeserverUrl, + userId: TEST_USER_ID, + accessToken: "akjgkrgjs", + deviceId: TEST_DEVICE_ID, + }); + + await initCrypto(aliceClient); + }); + + afterEach(async () => { + await aliceClient.stopClient(); + fetchMock.mockReset(); + }); + + describe("bootstrapCrossSigning (before initialsync completes)", () => { + it("publishes keys if none were yet published", async () => { + // have account_data requests return an empty object + fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {}); + + // we expect a request to upload signatures for our device ... + fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {}); + + // ... and one to upload the cross-signing keys (with UIA) + fetchMock.post( + // legacy crypto uses /unstable/; /v3/ is correct + { + url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"), + name: "upload-keys", + }, + {}, + ); + + // provide a UIA callback, so that the cross-signing keys are uploaded + const authDict = { type: "test" }; + const uiaCallback: UIAuthCallback = async (makeRequest) => { + await makeRequest(authDict); + }; + + // now bootstrap cross signing, and check it resolves successfully + await aliceClient.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: uiaCallback, + }); + + // check the cross-signing keys upload + expect(fetchMock.called("upload-keys")).toBeTruthy(); + const [, keysOpts] = fetchMock.lastCall("upload-keys")!; + const keysBody = JSON.parse(keysOpts!.body as string); + expect(keysBody.auth).toEqual(authDict); // check uia dict was passed + // there should be a key of each type + // master key is signed by the device + expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`); + const masterKeyId = Object.keys(keysBody.master_key.keys)[0]; + // ssk and usk are signed by the master key + expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`); + expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`); + const sskId = Object.keys(keysBody.self_signing_key.keys)[0]; + + // check the publish call + expect(fetchMock.called("upload-sigs")).toBeTruthy(); + const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!; + const body = JSON.parse(sigsOpts!.body as string); + // there should be a signature for our device, by our self-signing key. + expect(body).toHaveProperty( + `[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`, + ); + }); + }); +}); diff --git a/spec/integ/crypto.spec.ts b/spec/integ/crypto.spec.ts index 4e70bdd6e9f..951e853f90e 100644 --- a/spec/integ/crypto.spec.ts +++ b/spec/integ/crypto.spec.ts @@ -1111,7 +1111,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, } catch (e) { expect((e as any).name).toEqual("UnknownDeviceError"); expect([...(e as any).devices.keys()]).toEqual([aliceClient.getUserId()!]); - expect((e as any).devices.get(aliceClient.getUserId()!).has("DEVICE_ID")); + expect((e as any).devices.get(aliceClient.getUserId()!).has("DEVICE_ID")).toBeTruthy(); } // mark the device as known, and resend. diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index bb4e9563431..680e408380b 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -1142,7 +1142,7 @@ describe("MatrixClient event timelines", function () { const prom = emitPromise(room, ThreadEvent.Update); // Assume we're seeing the reply while loading backlog - room.addLiveEvents([THREAD_REPLY2]); + await room.addLiveEvents([THREAD_REPLY2]); httpBackend .when( "GET", @@ -1156,7 +1156,7 @@ describe("MatrixClient event timelines", function () { }); await flushHttp(prom); // but while loading the metadata, a new reply has arrived - room.addLiveEvents([THREAD_REPLY3]); + await room.addLiveEvents([THREAD_REPLY3]); const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!; // then the events should still be all in the right order expect(thread.events.map((it) => it.getId())).toEqual([ @@ -1248,7 +1248,7 @@ describe("MatrixClient event timelines", function () { const prom = emitPromise(room, ThreadEvent.Update); // Assume we're seeing the reply while loading backlog - room.addLiveEvents([THREAD_REPLY2]); + await room.addLiveEvents([THREAD_REPLY2]); httpBackend .when( "GET", @@ -1267,7 +1267,7 @@ describe("MatrixClient event timelines", function () { }); await flushHttp(prom); // but while loading the metadata, a new reply has arrived - room.addLiveEvents([THREAD_REPLY3]); + await room.addLiveEvents([THREAD_REPLY3]); const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!; // then the events should still be all in the right order expect(thread.events.map((it) => it.getId())).toEqual([ @@ -1572,7 +1572,7 @@ describe("MatrixClient event timelines", function () { respondToEvent(THREAD_ROOT_UPDATED); respondToEvent(THREAD_ROOT_UPDATED); respondToEvent(THREAD2_ROOT); - room.addLiveEvents([THREAD_REPLY2]); + await room.addLiveEvents([THREAD_REPLY2]); await httpBackend.flushAllExpected(); await prom; expect(thread.length).toBe(2); @@ -1937,11 +1937,6 @@ describe("MatrixClient event timelines", function () { .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/context/" + encodeURIComponent(THREAD_ROOT.event_id!)) .respond(200, function () { diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index 86228037a46..baec3039ff4 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -36,11 +36,15 @@ import { NotificationCountType, IEphemeral, Room, + IndexedDBStore, + RelationType, } 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"; +import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils"; +import { THREAD_RELATION_TYPE } from "../../src/models/thread"; describe("MatrixClient syncing", () => { const selfUserId = "@alice:localhost"; @@ -1867,4 +1871,124 @@ describe("MatrixClient syncing (IndexedDB version)", () => { idbClient.stopClient(); idbHttpBackend.stop(); }); + + it("should query server for which thread a 2nd order relation belongs to and stash in sync accumulator", async () => { + const roomId = "!room:example.org"; + + async function startClient(client: MatrixClient): Promise { + await Promise.all([ + idbClient.startClient({ + // Without this all events just go into the main timeline + threadSupport: true, + }), + idbHttpBackend.flushAllExpected(), + emitPromise(idbClient, ClientEvent.Room), + ]); + } + + function assertEventsExpected(client: MatrixClient): void { + const room = client.getRoom(roomId); + const mainTimelineEvents = room!.getLiveTimeline().getEvents(); + expect(mainTimelineEvents).toHaveLength(1); + expect(mainTimelineEvents[0].getContent().body).toEqual("Test"); + + const thread = room!.getThread("$someThreadId")!; + expect(thread.replayEvents).toHaveLength(1); + expect(thread.replayEvents![0].getRelation()!.key).toEqual("🪿"); + } + + let idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, { + store: new IndexedDBStore({ + indexedDB: global.indexedDB, + dbName: "test", + }), + }); + let idbHttpBackend = idbTestClient.httpBackend; + let idbClient = idbTestClient.client; + await idbClient.store.startup(); + + idbHttpBackend.when("GET", "/versions").respond(200, { versions: ["v1.4"] }); + idbHttpBackend.when("GET", "/pushrules/").respond(200, {}); + idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" }); + + const syncRoomSection = { + join: { + [roomId]: { + timeline: { + prev_batch: "foo", + events: [ + mkMessage({ + room: roomId, + user: selfUserId, + msg: "Test", + }), + mkEvent({ + room: roomId, + user: selfUserId, + content: { + "m.relates_to": { + rel_type: RelationType.Annotation, + event_id: "$someUnknownEvent", + key: "🪿", + }, + }, + type: "m.reaction", + }), + ], + }, + }, + }, + }; + idbHttpBackend.when("GET", "/sync").respond(200, { + ...syncData, + rooms: syncRoomSection, + }); + idbHttpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/event/%24someUnknownEvent`).respond( + 200, + mkEvent({ + room: roomId, + user: selfUserId, + content: { + "body": "Thread response", + "m.relates_to": { + rel_type: THREAD_RELATION_TYPE.name, + event_id: "$someThreadId", + }, + }, + type: "m.room.message", + }), + ); + + await startClient(idbClient); + assertEventsExpected(idbClient); + + idbHttpBackend.verifyNoOutstandingExpectation(); + // Force sync accumulator to persist, reset client, assert it doesn't re-fetch event on next start-up + await idbClient.store.save(true); + await idbClient.stopClient(); + await idbClient.store.destroy(); + await idbHttpBackend.stop(); + + idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, { + store: new IndexedDBStore({ + indexedDB: global.indexedDB, + dbName: "test", + }), + }); + idbHttpBackend = idbTestClient.httpBackend; + idbClient = idbTestClient.client; + await idbClient.store.startup(); + + idbHttpBackend.when("GET", "/versions").respond(200, { versions: ["v1.4"] }); + idbHttpBackend.when("GET", "/pushrules/").respond(200, {}); + idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" }); + idbHttpBackend.when("GET", "/sync").respond(200, syncData); + + await startClient(idbClient); + assertEventsExpected(idbClient); + + idbHttpBackend.verifyNoOutstandingExpectation(); + await idbClient.stopClient(); + await idbHttpBackend.stop(); + }); }); diff --git a/spec/integ/matrix-client-unread-notifications.spec.ts b/spec/integ/matrix-client-unread-notifications.spec.ts index 8274d7afaba..a4d3c2b8991 100644 --- a/spec/integ/matrix-client-unread-notifications.spec.ts +++ b/spec/integ/matrix-client-unread-notifications.spec.ts @@ -89,7 +89,7 @@ describe("MatrixClient syncing", () => { const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] }); const threadReply = thread.events.at(-1)!; - room.addLiveEvents([thread.rootEvent]); + await room.addLiveEvents([thread.rootEvent]); // Initialize read receipt datastructure before testing the reaction room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false); diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 6fdaeb119bd..dfec79e1583 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -42,6 +42,7 @@ import { SyncApiOptions, SyncState } from "../../src/sync"; import { IStoredClientOpts } from "../../src/client"; import { logger } from "../../src/logger"; import { emitPromise } from "../test-utils/test-utils"; +import { defer } from "../../src/utils"; describe("SlidingSyncSdk", () => { let client: MatrixClient | undefined; @@ -301,67 +302,57 @@ describe("SlidingSyncSdk", () => { }, }; - it("can be created with required_state and timeline", () => { + it("can be created with required_state and timeline", async () => { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]); + await emitPromise(client!, ClientEvent.Room); const gotRoom = client!.getRoom(roomA); - expect(gotRoom).toBeDefined(); - if (gotRoom == null) { - return; - } - expect(gotRoom.name).toEqual(data[roomA].name); - expect(gotRoom.getMyMembership()).toEqual("join"); - assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline); + expect(gotRoom).toBeTruthy(); + expect(gotRoom!.name).toEqual(data[roomA].name); + expect(gotRoom!.getMyMembership()).toEqual("join"); + assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline); }); - it("can be created with timeline only", () => { + it("can be created with timeline only", async () => { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]); + await emitPromise(client!, ClientEvent.Room); const gotRoom = client!.getRoom(roomB); - expect(gotRoom).toBeDefined(); - if (gotRoom == null) { - return; - } - expect(gotRoom.name).toEqual(data[roomB].name); - expect(gotRoom.getMyMembership()).toEqual("join"); - assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline); + expect(gotRoom).toBeTruthy(); + expect(gotRoom!.name).toEqual(data[roomB].name); + expect(gotRoom!.getMyMembership()).toEqual("join"); + assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline); }); - it("can be created with a highlight_count", () => { + it("can be created with a highlight_count", async () => { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]); + await emitPromise(client!, ClientEvent.Room); const gotRoom = client!.getRoom(roomC); - expect(gotRoom).toBeDefined(); - if (gotRoom == null) { - return; - } - expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual( + expect(gotRoom).toBeTruthy(); + expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual( data[roomC].highlight_count, ); }); - it("can be created with a notification_count", () => { + it("can be created with a notification_count", async () => { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]); + await emitPromise(client!, ClientEvent.Room); const gotRoom = client!.getRoom(roomD); - expect(gotRoom).toBeDefined(); - if (gotRoom == null) { - return; - } - expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual( + expect(gotRoom).toBeTruthy(); + expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual( data[roomD].notification_count, ); }); - it("can be created with an invited/joined_count", () => { + it("can be created with an invited/joined_count", async () => { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]); + await emitPromise(client!, ClientEvent.Room); const gotRoom = client!.getRoom(roomG); - expect(gotRoom).toBeDefined(); - if (gotRoom == null) { - return; - } - expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count); - expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count); + expect(gotRoom).toBeTruthy(); + expect(gotRoom!.getInvitedMemberCount()).toEqual(data[roomG].invited_count); + expect(gotRoom!.getJoinedMemberCount()).toEqual(data[roomG].joined_count); }); - it("can be created with live events", () => { - let seenLiveEvent = false; + it("can be created with live events", async () => { + const seenLiveEventDeferred = defer(); const listener = ( ev: MatrixEvent, room?: Room, @@ -371,43 +362,37 @@ describe("SlidingSyncSdk", () => { ) => { if (timelineData?.liveEvent) { assertTimelineEvents([ev], data[roomH].timeline.slice(-1)); - seenLiveEvent = true; + seenLiveEventDeferred.resolve(true); } }; client!.on(RoomEvent.Timeline, listener); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]); + await emitPromise(client!, ClientEvent.Room); 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"); + expect(gotRoom).toBeTruthy(); + 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); + assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), data[roomH].timeline); + await expect(seenLiveEventDeferred.promise).resolves.toBeTruthy(); }); - it("can be created with invite_state", () => { + it("can be created with invite_state", async () => { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]); + await emitPromise(client!, ClientEvent.Room); const gotRoom = client!.getRoom(roomE); - expect(gotRoom).toBeDefined(); - if (gotRoom == null) { - return; - } - expect(gotRoom.getMyMembership()).toEqual("invite"); - expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite); + expect(gotRoom).toBeTruthy(); + expect(gotRoom!.getMyMembership()).toEqual("invite"); + expect(gotRoom!.currentState.getJoinRule()).toEqual(JoinRule.Invite); }); - it("uses the 'name' field to caluclate the room name", () => { + it("uses the 'name' field to caluclate the room name", async () => { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]); + await emitPromise(client!, ClientEvent.Room); const gotRoom = client!.getRoom(roomF); - expect(gotRoom).toBeDefined(); - if (gotRoom == null) { - return; - } - expect(gotRoom.name).toEqual(data[roomF].name); + expect(gotRoom).toBeTruthy(); + expect(gotRoom!.name).toEqual(data[roomF].name); }); describe("updating", () => { @@ -419,33 +404,33 @@ describe("SlidingSyncSdk", () => { name: data[roomA].name, }); const gotRoom = client!.getRoom(roomA); - expect(gotRoom).toBeDefined(); + expect(gotRoom).toBeTruthy(); if (gotRoom == null) { return; } const newTimeline = data[roomA].timeline; newTimeline.push(newEvent); - assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline); + assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-3), newTimeline); }); it("can update with a new required_state event", async () => { let gotRoom = client!.getRoom(roomB); - expect(gotRoom).toBeDefined(); + expect(gotRoom).toBeTruthy(); if (gotRoom == null) { return; } - expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default + expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Invite); // default mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, { required_state: [mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, "")], timeline: [], name: data[roomB].name, }); gotRoom = client!.getRoom(roomB); - expect(gotRoom).toBeDefined(); + expect(gotRoom).toBeTruthy(); if (gotRoom == null) { return; } - expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted); + expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Restricted); }); it("can update with a new highlight_count", async () => { @@ -456,11 +441,11 @@ describe("SlidingSyncSdk", () => { highlight_count: 1, }); const gotRoom = client!.getRoom(roomC); - expect(gotRoom).toBeDefined(); + expect(gotRoom).toBeTruthy(); if (gotRoom == null) { return; } - expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1); + expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1); }); it("can update with a new notification_count", async () => { @@ -471,11 +456,11 @@ describe("SlidingSyncSdk", () => { notification_count: 1, }); const gotRoom = client!.getRoom(roomD); - expect(gotRoom).toBeDefined(); + expect(gotRoom).toBeTruthy(); if (gotRoom == null) { return; } - expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1); + expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1); }); it("can update with a new joined_count", () => { @@ -486,11 +471,11 @@ describe("SlidingSyncSdk", () => { joined_count: 1, }); const gotRoom = client!.getRoom(roomG); - expect(gotRoom).toBeDefined(); + expect(gotRoom).toBeTruthy(); if (gotRoom == null) { return; } - expect(gotRoom.getJoinedMemberCount()).toEqual(1); + expect(gotRoom!.getJoinedMemberCount()).toEqual(1); }); // Regression test for a bug which caused the timeline entries to be out-of-order @@ -512,7 +497,7 @@ describe("SlidingSyncSdk", () => { initial: true, // e.g requested via room subscription }); const gotRoom = client!.getRoom(roomA); - expect(gotRoom).toBeDefined(); + expect(gotRoom).toBeTruthy(); if (gotRoom == null) { return; } @@ -530,7 +515,7 @@ describe("SlidingSyncSdk", () => { ); // we expect the timeline now to be oldTimeline (so the old events are in fact old) - assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), oldTimeline); + assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), oldTimeline); }); }); }); @@ -626,9 +611,9 @@ describe("SlidingSyncSdk", () => { await httpBackend!.flush("/profile", 1, 1000); await emitPromise(client!, RoomMemberEvent.Name); const room = client!.getRoom(roomId)!; - expect(room).toBeDefined(); + expect(room).toBeTruthy(); const inviteeMember = room.getMember(invitee)!; - expect(inviteeMember).toBeDefined(); + expect(inviteeMember).toBeTruthy(); expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url); expect(inviteeMember.name).toEqual(inviteeProfile.displayname); }); @@ -723,7 +708,7 @@ describe("SlidingSyncSdk", () => { ], }); globalData = client!.getAccountData(globalType)!; - expect(globalData).toBeDefined(); + expect(globalData).toBeTruthy(); expect(globalData.getContent()).toEqual(globalContent); }); @@ -744,6 +729,7 @@ describe("SlidingSyncSdk", () => { foo: "bar", }; const roomType = "test"; + await emitPromise(client!, ClientEvent.Room); ext.onResponse({ rooms: { [roomId]: [ @@ -755,9 +741,9 @@ describe("SlidingSyncSdk", () => { }, }); const room = client!.getRoom(roomId)!; - expect(room).toBeDefined(); + expect(room).toBeTruthy(); const event = room.getAccountData(roomType)!; - expect(event).toBeDefined(); + expect(event).toBeTruthy(); expect(event.getContent()).toEqual(roomContent); }); @@ -943,8 +929,9 @@ describe("SlidingSyncSdk", () => { ], initial: true, }); + await emitPromise(client!, ClientEvent.Room); const room = client!.getRoom(roomId)!; - expect(room).toBeDefined(); + expect(room).toBeTruthy(); expect(room.getMember(selfUserId)?.typing).toEqual(false); ext.onResponse({ rooms: { @@ -984,7 +971,7 @@ describe("SlidingSyncSdk", () => { initial: true, }); const room = client!.getRoom(roomId)!; - expect(room).toBeDefined(); + expect(room).toBeTruthy(); expect(room.getMember(selfUserId)?.typing).toEqual(false); ext.onResponse({ rooms: { @@ -1077,12 +1064,13 @@ describe("SlidingSyncSdk", () => { ], initial: true, }); + await emitPromise(client!, ClientEvent.Room); const room = client!.getRoom(roomId)!; - expect(room).toBeDefined(); + expect(room).toBeTruthy(); expect(room.getReadReceiptForUserId(alice, true)).toBeNull(); ext.onResponse(generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567)); const receipt = room.getReadReceiptForUserId(alice); - expect(receipt).toBeDefined(); + expect(receipt).toBeTruthy(); expect(receipt?.eventId).toEqual(lastEvent.event_id); expect(receipt?.data.ts).toEqual(1234567); expect(receipt?.data.thread_id).toBeFalsy(); diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 38094be450a..c37a7e69172 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -1698,7 +1698,7 @@ describe("SlidingSync", () => { }); function timeout(delayMs: number, reason: string): { promise: Promise; cancel: () => void } { - let timeoutId: NodeJS.Timeout; + let timeoutId: ReturnType; return { promise: new Promise((resolve, reject) => { timeoutId = setTimeout(() => { diff --git a/spec/olm-loader.ts b/spec/olm-loader.ts index a139a48672b..29817176bd4 100644 --- a/spec/olm-loader.ts +++ b/spec/olm-loader.ts @@ -23,5 +23,5 @@ try { global.Olm = require("@matrix-org/olm"); logger.log("loaded libolm"); } catch (e) { - logger.warn("unable to run crypto tests: libolm not available"); + logger.warn("unable to run crypto tests: libolm not available", e); } diff --git a/spec/test-utils/test-utils.ts b/spec/test-utils/test-utils.ts index d164272d91b..dab7dfc9b85 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -6,7 +6,7 @@ import "../olm-loader"; import { logger } from "../../src/logger"; import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event"; -import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src"; +import { ClientEvent, EventType, IPusher, MatrixClient, MsgType, RelationType } from "../../src"; import { SyncState } from "../../src/sync"; import { eventMapperFor } from "../../src/event-mapper"; @@ -258,6 +258,9 @@ export interface IMessageOpts { * @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 opts.relatesTo - An IEventRelation relating this to another event. + * @param opts.ts - The timestamp of 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 */ @@ -297,6 +300,7 @@ interface IReplyMessageOpts extends IMessageOpts { * @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.ts - The timestamp of 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. @@ -330,6 +334,73 @@ export function mkReplyMessage( return mkEvent(eventOpts, client); } +/** + * Create a reaction event. + * + * @param target - the event we are reacting to. + * @param client - the MatrixClient + * @param userId - the userId of the sender + * @param roomId - the id of the room we are in + * @param ts - The timestamp of the event. + * @returns The event + */ +export function mkReaction( + target: MatrixEvent, + client: MatrixClient, + userId: string, + roomId: string, + ts?: number, +): MatrixEvent { + return mkEvent( + { + event: true, + type: EventType.Reaction, + user: userId, + room: roomId, + content: { + "m.relates_to": { + rel_type: RelationType.Annotation, + event_id: target.getId()!, + key: Math.random().toString(), + }, + }, + ts, + }, + client, + ); +} + +export function mkEdit( + target: MatrixEvent, + client: MatrixClient, + userId: string, + roomId: string, + msg?: string, + ts?: number, +) { + msg = msg ?? `Edit of ${target.getId()}`; + return mkEvent( + { + event: true, + type: EventType.RoomMessage, + user: userId, + room: roomId, + content: { + "body": `* ${msg}`, + "m.new_content": { + body: msg, + }, + "m.relates_to": { + rel_type: RelationType.Replace, + event_id: target.getId()!, + }, + }, + ts, + }, + client, + ); +} + /** * A mock implementation of webstorage */ diff --git a/spec/test-utils/thread.ts b/spec/test-utils/thread.ts index 6232f20d70c..ebca5720fe4 100644 --- a/spec/test-utils/thread.ts +++ b/spec/test-utils/thread.ts @@ -115,6 +115,26 @@ type MakeThreadProps = { ts?: number; }; +type MakeThreadResult = { + /** + * Thread model + */ + thread: Thread; + /** + * Thread root event + */ + rootEvent: MatrixEvent; + /** + * Events added to the thread + */ + events: MatrixEvent[]; +}; + +/** + * Starts a new thread in a room by creating a message as thread root. + * Also creates a Thread model and adds it to the room. + * Does not insert the messages into a timeline. + */ export const mkThread = ({ room, client, @@ -122,7 +142,7 @@ export const mkThread = ({ participantUserIds, length = 2, ts = 1, -}: MakeThreadProps): { thread: Thread; rootEvent: MatrixEvent; events: MatrixEvent[] } => { +}: MakeThreadProps): MakeThreadResult => { const { rootEvent, events } = makeThreadEvents({ roomId: room.roomId, authorId, diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index e50a8df45d7..2037767cc94 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.ts @@ -239,6 +239,8 @@ export class MockRTCPeerConnection { public triggerIncomingDataChannel(): void { this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent); } + + public restartIce(): void {} } export class MockRTCRtpSender { diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index e62bb2eac1e..b96c0668656 100644 --- a/spec/unit/crypto.spec.ts +++ b/spec/unit/crypto.spec.ts @@ -381,12 +381,7 @@ describe("Crypto", function () { event.senderCurve25519Key = null; // @ts-ignore private properties event.claimedEd25519Key = null; - try { - await bobClient.crypto!.decryptEvent(event); - } catch (e) { - // we expect this to fail because we don't have the - // decryption keys yet - } + await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy(); }), ); @@ -617,12 +612,7 @@ describe("Crypto", function () { event.senderCurve25519Key = null; // @ts-ignore private properties event.claimedEd25519Key = null; - try { - await secondAliceClient.crypto!.decryptEvent(event); - } catch (e) { - // we expect this to fail because we don't have the - // decryption keys yet - } + await expect(secondAliceClient.crypto!.decryptEvent(event)).rejects.toBeTruthy(); }), ); @@ -725,12 +715,7 @@ describe("Crypto", function () { event.senderCurve25519Key = null; // @ts-ignore private properties event.claimedEd25519Key = null; - try { - await bobClient.crypto!.decryptEvent(event); - } catch (e) { - // we expect this to fail because we don't have the - // decryption keys yet - } + await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy(); }), ); @@ -805,12 +790,7 @@ describe("Crypto", function () { event.senderCurve25519Key = null; // @ts-ignore private properties event.claimedEd25519Key = null; - try { - await bobClient.crypto!.decryptEvent(event); - } catch (e) { - // we expect this to fail because we don't have the - // decryption keys yet - } + await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy(); }), ); @@ -897,12 +877,7 @@ describe("Crypto", function () { event.senderCurve25519Key = null; // @ts-ignore private properties event.claimedEd25519Key = null; - try { - await bobClient.crypto!.decryptEvent(event); - } catch (e) { - // we expect this to fail because we don't have the - // decryption keys yet - } + await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy(); }), ); diff --git a/spec/unit/crypto/cross-signing.spec.ts b/spec/unit/crypto/cross-signing.spec.ts index eeee27566d1..276c2feaf41 100644 --- a/spec/unit/crypto/cross-signing.spec.ts +++ b/spec/unit/crypto/cross-signing.spec.ts @@ -24,10 +24,11 @@ import * as olmlib from "../../../src/crypto/olmlib"; import { MatrixError } from "../../../src/http-api"; import { logger } from "../../../src/logger"; import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client"; -import { CryptoEvent, IBootstrapCrossSigningOpts } from "../../../src/crypto"; +import { CryptoEvent } from "../../../src/crypto"; import { IDevice } from "../../../src/crypto/deviceinfo"; import { TestClient } from "../../TestClient"; import { resetCrossSigningKeys } from "./crypto-utils"; +import { BootstrapCrossSigningOpts } from "../../../src/crypto-api"; const PUSH_RULES_RESPONSE: Response = { method: "GET", @@ -146,7 +147,7 @@ describe("Cross Signing", function () { alice.uploadKeySignatures = async () => ({ failures: {} }); alice.setAccountData = async () => ({}); alice.getAccountDataFromServer = async (): Promise => ({} as T); - const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => { + const authUploadDeviceSigningKeys: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => { await func({}); }; diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts index 91b8deb1cb6..3e95ed05e21 100644 --- a/spec/unit/crypto/secrets.spec.ts +++ b/spec/unit/crypto/secrets.spec.ts @@ -148,22 +148,14 @@ describe("Secrets", function () { it("should throw if given a key that doesn't exist", async function () { const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - try { - await alice.storeSecret("foo", "bar", ["this secret does not exist"]); - // should be able to use expect(...).toThrow() but mocha still fails - // the test even when it throws for reasons I have no inclination to debug - expect(true).toBeFalsy(); - } catch (e) {} + await expect(alice.storeSecret("foo", "bar", ["this secret does not exist"])).rejects.toBeTruthy(); alice.stopClient(); }); it("should refuse to encrypt with zero keys", async function () { const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - try { - await alice.storeSecret("foo", "bar", []); - expect(true).toBeFalsy(); - } catch (e) {} + await expect(alice.storeSecret("foo", "bar", [])).rejects.toBeTruthy(); alice.stopClient(); }); @@ -214,10 +206,7 @@ describe("Secrets", function () { it("should refuse to encrypt if no keys given and no default key", async function () { const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); - try { - await alice.storeSecret("foo", "bar"); - expect(true).toBeFalsy(); - } catch (e) {} + await expect(alice.storeSecret("foo", "bar")).rejects.toBeTruthy(); alice.stopClient(); }); diff --git a/spec/unit/crypto/verification/sas.spec.ts b/spec/unit/crypto/verification/sas.spec.ts index 1ea2c322285..ba73ced6a3e 100644 --- a/spec/unit/crypto/verification/sas.spec.ts +++ b/spec/unit/crypto/verification/sas.spec.ts @@ -144,7 +144,7 @@ describe("SAS verification", function () { expect(e.sas).toEqual(aliceSasEvent.sas); e.confirm(); aliceSasEvent.confirm(); - } catch (error) { + } catch { e.mismatch(); aliceSasEvent.mismatch(); } @@ -169,7 +169,7 @@ describe("SAS verification", function () { expect(e.sas).toEqual(bobSasEvent.sas); e.confirm(); bobSasEvent.confirm(); - } catch (error) { + } catch { e.mismatch(); bobSasEvent.mismatch(); } @@ -519,7 +519,7 @@ describe("SAS verification", function () { expect(e.sas).toEqual(aliceSasEvent.sas); e.confirm(); aliceSasEvent.confirm(); - } catch (error) { + } catch { e.mismatch(); aliceSasEvent.mismatch(); } @@ -543,7 +543,7 @@ describe("SAS verification", function () { expect(e.sas).toEqual(bobSasEvent.sas); e.confirm(); bobSasEvent.confirm(); - } catch (error) { + } catch { e.mismatch(); bobSasEvent.mismatch(); } diff --git a/spec/unit/event-timeline-set.spec.ts b/spec/unit/event-timeline-set.spec.ts index a099aba0370..7afee718967 100644 --- a/spec/unit/event-timeline-set.spec.ts +++ b/spec/unit/event-timeline-set.spec.ts @@ -24,13 +24,10 @@ import { MatrixClient, MatrixEvent, MatrixEventEvent, - RelationType, Room, - RoomEvent, } from "../../src"; -import { FeatureSupport, Thread } from "../../src/models/thread"; +import { Thread } from "../../src/models/thread"; import { ReEmitter } from "../../src/ReEmitter"; -import { eventMapperFor } from "../../src/event-mapper"; describe("EventTimelineSet", () => { const roomId = "!foo:bar"; @@ -206,88 +203,6 @@ describe("EventTimelineSet", () => { expect(liveTimeline.getEvents().length).toStrictEqual(0); }); - it("should allow edits to be added to thread timeline", async () => { - jest.spyOn(client, "supportsThreads").mockReturnValue(true); - jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {})); - Thread.hasServerSideSupport = FeatureSupport.Stable; - - const sender = "@alice:matrix.org"; - - const root = utils.mkEvent({ - event: true, - content: { - body: "Thread root", - }, - type: EventType.RoomMessage, - sender, - }); - room.addLiveEvents([root]); - - const threadReply = utils.mkEvent({ - event: true, - content: { - "body": "Thread reply", - "m.relates_to": { - event_id: root.getId()!, - rel_type: RelationType.Thread, - }, - }, - type: EventType.RoomMessage, - sender, - }); - - root.setUnsigned({ - "m.relations": { - [RelationType.Thread]: { - count: 1, - latest_event: { - content: threadReply.getContent(), - origin_server_ts: 5, - room_id: room.roomId, - sender, - type: EventType.RoomMessage, - event_id: threadReply.getId()!, - user_id: sender, - age: 1, - }, - current_user_participated: true, - }, - }, - }); - - const editToThreadReply = utils.mkEvent({ - event: true, - content: { - "body": " * edit", - "m.new_content": { - "body": "edit", - "msgtype": "m.text", - "org.matrix.msc1767.text": "edit", - }, - "m.relates_to": { - event_id: threadReply.getId()!, - rel_type: RelationType.Replace, - }, - }, - type: EventType.RoomMessage, - sender, - }); - - jest.spyOn(client, "paginateEventTimeline").mockImplementation(async () => { - thread.timelineSet.getLiveTimeline().addEvent(threadReply, { toStartOfTimeline: true }); - return true; - }); - jest.spyOn(client, "relations").mockResolvedValue({ - events: [], - }); - - const thread = room.createThread(root.getId()!, root, [threadReply, editToThreadReply], false); - thread.once(RoomEvent.TimelineReset, () => { - const lastEvent = thread.timeline.at(-1)!; - expect(lastEvent.getContent().body).toBe(" * edit"); - }); - }); - describe("non-room timeline", () => { it("Adds event to timeline", () => { const nonRoomEventTimelineSet = new EventTimelineSet( diff --git a/spec/unit/interactive-auth.spec.ts b/spec/unit/interactive-auth.spec.ts index 48279530289..554d747352d 100644 --- a/spec/unit/interactive-auth.spec.ts +++ b/spec/unit/interactive-auth.spec.ts @@ -517,4 +517,47 @@ describe("InteractiveAuth", () => { expect(ia.getEmailSid()).toEqual(sid); }); }); + + it("should prioritise shorter flows", async () => { + const doRequest = jest.fn(); + const stateUpdated = jest.fn(); + + const ia = new InteractiveAuth({ + matrixClient: getFakeClient(), + doRequest: doRequest, + stateUpdated: stateUpdated, + requestEmailToken: jest.fn(), + authData: { + session: "sessionId", + flows: [{ stages: [AuthType.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }], + params: {}, + }, + }); + + // @ts-ignore + ia.chooseStage(); + expect(ia.getChosenFlow()?.stages).toEqual([AuthType.Password]); + }); + + it("should prioritise flows with entirely supported stages", async () => { + const doRequest = jest.fn(); + const stateUpdated = jest.fn(); + + const ia = new InteractiveAuth({ + matrixClient: getFakeClient(), + doRequest: doRequest, + stateUpdated: stateUpdated, + requestEmailToken: jest.fn(), + authData: { + session: "sessionId", + flows: [{ stages: ["com.devture.shared_secret_auth"] }, { stages: [AuthType.Password] }], + params: {}, + }, + supportedStages: [AuthType.Password], + }); + + // @ts-ignore + ia.chooseStage(); + expect(ia.getChosenFlow()?.stages).toEqual([AuthType.Password]); + }); }); diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 635cd2ee5cd..e77a3d5e8a1 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -70,6 +70,7 @@ import { SyncState } from "../../src/sync"; import * as featureUtils from "../../src/feature"; import { StubStore } from "../../src/store/stub"; import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from "../../src/secret-storage"; +import { CryptoBackend } from "../../src/common-crypto/CryptoBackend"; jest.useFakeTimers(); @@ -2750,6 +2751,60 @@ describe("MatrixClient", function () { }); }); + // these wrappers are deprecated, but we need coverage of them to pass the quality gate + describe("Crypto wrappers", () => { + describe("exception if no crypto", () => { + it("isCrossSigningReady", () => { + expect(() => client.isCrossSigningReady()).toThrow("End-to-end encryption disabled"); + }); + + it("bootstrapCrossSigning", () => { + expect(() => client.bootstrapCrossSigning({})).toThrow("End-to-end encryption disabled"); + }); + + it("isSecretStorageReady", () => { + expect(() => client.isSecretStorageReady()).toThrow("End-to-end encryption disabled"); + }); + }); + + describe("defer to crypto backend", () => { + let mockCryptoBackend: Mocked; + + beforeEach(() => { + mockCryptoBackend = { + isCrossSigningReady: jest.fn(), + bootstrapCrossSigning: jest.fn(), + isSecretStorageReady: jest.fn(), + stop: jest.fn().mockResolvedValue(undefined), + } as unknown as Mocked; + client["cryptoBackend"] = mockCryptoBackend; + }); + + it("isCrossSigningReady", async () => { + const testResult = "test"; + mockCryptoBackend.isCrossSigningReady.mockResolvedValue(testResult as unknown as boolean); + expect(await client.isCrossSigningReady()).toBe(testResult); + expect(mockCryptoBackend.isCrossSigningReady).toHaveBeenCalledTimes(1); + }); + + it("bootstrapCrossSigning", async () => { + const testOpts = {}; + mockCryptoBackend.bootstrapCrossSigning.mockResolvedValue(undefined); + await client.bootstrapCrossSigning(testOpts); + expect(mockCryptoBackend.bootstrapCrossSigning).toHaveBeenCalledTimes(1); + expect(mockCryptoBackend.bootstrapCrossSigning).toHaveBeenCalledWith(testOpts); + }); + + it("isSecretStorageReady", async () => { + client["cryptoBackend"] = mockCryptoBackend; + const testResult = "test"; + mockCryptoBackend.isSecretStorageReady.mockResolvedValue(testResult as unknown as boolean); + expect(await client.isSecretStorageReady()).toBe(testResult); + expect(mockCryptoBackend.isSecretStorageReady).toHaveBeenCalledTimes(1); + }); + }); + }); + describe("paginateEventTimeline()", () => { describe("notifications timeline", () => { const unsafeNotification = { diff --git a/spec/unit/models/thread.spec.ts b/spec/unit/models/thread.spec.ts index d1e25c84274..d8dd88809b4 100644 --- a/spec/unit/models/thread.spec.ts +++ b/spec/unit/models/thread.spec.ts @@ -14,17 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { mocked } from "jest-mock"; + import { MatrixClient, PendingEventOrdering } from "../../../src/client"; -import { Room } from "../../../src/models/room"; -import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread"; -import { mkThread } from "../../test-utils/thread"; +import { Room, RoomEvent } from "../../../src/models/room"; +import { Thread, THREAD_RELATION_TYPE, ThreadEvent, FeatureSupport } from "../../../src/models/thread"; +import { makeThreadEvent, mkThread } from "../../test-utils/thread"; import { TestClient } from "../../TestClient"; -import { emitPromise, mkMessage, mock } from "../../test-utils/test-utils"; -import { Direction, EventStatus, MatrixEvent } from "../../../src"; +import { emitPromise, mkEdit, mkMessage, mkReaction, mock } from "../../test-utils/test-utils"; +import { Direction, EventStatus, EventType, MatrixEvent } from "../../../src"; import { ReceiptType } from "../../../src/@types/read_receipts"; import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client"; import { ReEmitter } from "../../../src/ReEmitter"; import { Feature, ServerSupport } from "../../../src/feature"; +import { eventMapperFor } from "../../../src/event-mapper"; describe("Thread", () => { describe("constructor", () => { @@ -424,4 +427,301 @@ describe("Thread", () => { expect(mock).toHaveBeenCalledWith("b1", "f1"); }); }); + + describe("insertEventIntoTimeline", () => { + it("Inserts a reaction in timestamp order", () => { + // Assumption: no server side support because if we have it, events + // can only be added to the timeline after the thread has been + // initialised, and we are not properly initialising it here. + expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None); + + const client = createClientWithEventMapper(); + const userId = "user1"; + const room = new Room("room1", client, userId); + + // Given a thread with a root plus 5 messages + const { thread, events } = mkThread({ + room, + client, + authorId: userId, + participantUserIds: ["@bob:hs", "@chia:hs", "@dv:hs"], + length: 6, + ts: 100, // Events will be at ts 100, 101, 102, 103, 104 and 105 + }); + + // When we insert a reaction to the second thread message + const replyEvent = mkReaction(events[2], client, userId, room.roomId, 104); + thread.insertEventIntoTimeline(replyEvent); + + // Then the reaction is inserted based on its timestamp + expect(thread.events.map((ev) => ev.getId())).toEqual([ + events[0].getId(), + events[1].getId(), + events[2].getId(), + events[3].getId(), + events[4].getId(), + replyEvent.getId(), + events[5].getId(), + ]); + }); + + describe("Without relations recursion support", () => { + it("Creates a local echo receipt for new events", async () => { + // Assumption: no server side support because if we have it, events + // can only be added to the timeline after the thread has been + // initialised, and we are not properly initialising it here. + expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None); + + // Given a client without relations recursion support + const client = createClientWithEventMapper(); + + // And a thread with an added event (with later timestamp) + const userId = "user1"; + const { thread, message } = await createThreadAndEvent(client, 1, 100, userId); + + // Then a receipt was added to the thread + const receipt = thread.getReadReceiptForUserId(userId); + expect(receipt).toBeTruthy(); + expect(receipt?.eventId).toEqual(message.getId()); + expect(receipt?.data.ts).toEqual(100); + expect(receipt?.data.thread_id).toEqual(thread.id); + + // (And the receipt was synthetic) + expect(thread.getReadReceiptForUserId(userId, true)).toBeNull(); + }); + + it("Doesn't create a local echo receipt for events before an existing receipt", async () => { + // Assumption: no server side support because if we have it, events + // can only be added to the timeline after the thread has been + // initialised, and we are not properly initialising it here. + expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None); + + // Given a client without relations recursion support + const client = createClientWithEventMapper(); + + // And a thread with an added event with a lower timestamp than its other events + const userId = "user1"; + const { thread } = await createThreadAndEvent(client, 200, 100, userId); + + // Then no receipt was added to the thread (the receipt is still + // for the thread root). This happens because since we have no + // recursive relations support, we know that sometimes events + // appear out of order, so we have to check their timestamps as + // a guess of the correct order. + expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(thread.rootEvent?.getId()); + }); + }); + + describe("With relations recursion support", () => { + it("Creates a local echo receipt for new events", async () => { + // Assumption: no server side support because if we have it, events + // can only be added to the timeline after the thread has been + // initialised, and we are not properly initialising it here. + expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None); + + // Given a client WITH relations recursion support + const client = createClientWithEventMapper( + new Map([[Feature.RelationsRecursion, ServerSupport.Stable]]), + ); + + // And a thread with an added event (with later timestamp) + const userId = "user1"; + const { thread, message } = await createThreadAndEvent(client, 1, 100, userId); + + // Then a receipt was added to the thread + const receipt = thread.getReadReceiptForUserId(userId); + expect(receipt?.eventId).toEqual(message.getId()); + }); + + it("Creates a local echo receipt even for events BEFORE an existing receipt", async () => { + // Assumption: no server side support because if we have it, events + // can only be added to the timeline after the thread has been + // initialised, and we are not properly initialising it here. + expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None); + + // Given a client WITH relations recursion support + const client = createClientWithEventMapper( + new Map([[Feature.RelationsRecursion, ServerSupport.Stable]]), + ); + + // And a thread with an added event with a lower timestamp than its other events + const userId = "user1"; + const { thread, message } = await createThreadAndEvent(client, 200, 100, userId); + + // Then a receipt was added to the thread, because relations + // recursion is available, so we trust the server to have + // provided us with events in the right order. + const receipt = thread.getReadReceiptForUserId(userId); + expect(receipt?.eventId).toEqual(message.getId()); + }); + }); + + async function createThreadAndEvent( + client: MatrixClient, + rootTs: number, + eventTs: number, + userId: string, + ): Promise<{ thread: Thread; message: MatrixEvent }> { + const room = new Room("room1", client, userId); + + // Given a thread + const { thread } = mkThread({ + room, + client, + authorId: userId, + participantUserIds: [], + ts: rootTs, + }); + // Sanity: the current receipt is for the thread root + expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(thread.rootEvent?.getId()); + + const awaitTimelineEvent = new Promise((res) => thread.on(RoomEvent.Timeline, () => res())); + + // When we add a message that is before the latest receipt + const message = makeThreadEvent({ + event: true, + rootEventId: thread.id, + replyToEventId: thread.id, + user: userId, + room: room.roomId, + ts: eventTs, + }); + await thread.addEvent(message, false, true); + await awaitTimelineEvent; + + return { thread, message }; + } + + function createClientWithEventMapper(canSupport: Map = new Map()): MatrixClient { + const client = mock(MatrixClient, "MatrixClient"); + client.reEmitter = mock(ReEmitter, "ReEmitter"); + client.canSupport = canSupport; + jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {})); + mocked(client.supportsThreads).mockReturnValue(true); + return client; + } + }); + + describe("Editing events", () => { + describe("Given server support for threads", () => { + let previousThreadHasServerSideSupport: FeatureSupport; + + beforeAll(() => { + previousThreadHasServerSideSupport = Thread.hasServerSideSupport; + Thread.hasServerSideSupport = FeatureSupport.Stable; + }); + + afterAll(() => { + Thread.hasServerSideSupport = previousThreadHasServerSideSupport; + }); + + it("Adds edits from sync to the thread timeline and applies them", async () => { + // Given a thread + const client = createClient(); + const user = "@alice:matrix.org"; + const room = "!room:z"; + const thread = await createThread(client, user, room); + + // When a message and an edit are added to the thread + const messageToEdit = createThreadMessage(thread.id, user, room, "Thread reply"); + const editEvent = mkEdit(messageToEdit, client, user, room, "edit"); + await thread.addEvent(messageToEdit, false); + await thread.addEvent(editEvent, false); + + // Then both events end up in the timeline + const lastEvent = thread.timeline.at(-1)!; + const secondLastEvent = thread.timeline.at(-2)!; + expect(lastEvent).toBe(editEvent); + expect(secondLastEvent).toBe(messageToEdit); + + // And the first message has been edited + expect(secondLastEvent.getContent().body).toEqual("edit"); + }); + + it("Adds edits fetched on demand to the thread timeline and applies them", async () => { + // Given we don't support recursive relations + const client = createClient(new Map([[Feature.RelationsRecursion, ServerSupport.Unsupported]])); + // And we have a thread + const user = "@alice:matrix.org"; + const room = "!room:z"; + const thread = await createThread(client, user, room); + + // When a message is added to the thread, and an edit to it is provided on demand + const messageToEdit = createThreadMessage(thread.id, user, room, "Thread reply"); + // (fetchEditsWhereNeeded only applies to encrypted messages for some reason) + messageToEdit.event.type = EventType.RoomMessageEncrypted; + const editEvent = mkEdit(messageToEdit, client, user, room, "edit"); + mocked(client.relations).mockImplementation(async (_roomId, eventId) => { + if (eventId === messageToEdit.getId()) { + return { events: [editEvent] }; + } else { + return { events: [] }; + } + }); + await thread.addEvent(messageToEdit, false); + + // Then both events end up in the timeline + const lastEvent = thread.timeline.at(-1)!; + const secondLastEvent = thread.timeline.at(-2)!; + expect(lastEvent).toBe(editEvent); + expect(secondLastEvent).toBe(messageToEdit); + + // And the first message has been edited + expect(secondLastEvent.getContent().body).toEqual("edit"); + }); + }); + }); }); + +/** + * Create a message event that lives in a thread + */ +function createThreadMessage(threadId: string, user: string, room: string, msg: string): MatrixEvent { + return makeThreadEvent({ + event: true, + user, + room, + msg, + rootEventId: threadId, + replyToEventId: threadId, + }); +} + +/** + * Create a thread and wait for it to be properly initialised (so you can safely + * add events to it and expect them to appear in the timeline. + */ +async function createThread(client: MatrixClient, user: string, roomId: string): Promise { + const root = mkMessage({ event: true, user, room: roomId, msg: "Thread root" }); + const room = new Room(roomId, client, "@roomcreator:x"); + + // Ensure the root is in the room timeline + root.setThreadId(root.getId()); + await room.addLiveEvents([root]); + + // Create the thread and wait for it to be initialised + const thread = room.createThread(root.getId()!, root, [], false); + await new Promise((res) => thread.once(RoomEvent.TimelineReset, () => res())); + + return thread; +} + +/** + * Create a MatrixClient that supports threads and has all the methods used when + * creating a thread that call out to HTTP endpoints mocked out. + */ +function createClient(canSupport = new Map()): MatrixClient { + const client = mock(MatrixClient, "MatrixClient"); + client.reEmitter = mock(ReEmitter, "ReEmitter"); + client.canSupport = canSupport; + + jest.spyOn(client, "supportsThreads").mockReturnValue(true); + jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {})); + + // Mock methods that call out to HTTP endpoints + jest.spyOn(client, "paginateEventTimeline").mockResolvedValue(true); + jest.spyOn(client, "relations").mockResolvedValue({ events: [] }); + jest.spyOn(client, "fetchRoomEvent").mockResolvedValue({}); + + return client; +} diff --git a/spec/unit/pushprocessor.spec.ts b/spec/unit/pushprocessor.spec.ts index 36153e7deea..c381c5e6406 100644 --- a/spec/unit/pushprocessor.spec.ts +++ b/spec/unit/pushprocessor.spec.ts @@ -97,51 +97,6 @@ describe("NotificationService", function () { pattern: "foo*bar", rule_id: "foobar", }, - { - actions: [ - "notify", - { - set_tweak: "sound", - value: "default", - }, - { - set_tweak: "highlight", - }, - ], - enabled: true, - pattern: "p[io]ng", - rule_id: "pingpong", - }, - { - actions: [ - "notify", - { - set_tweak: "sound", - value: "default", - }, - { - set_tweak: "highlight", - }, - ], - enabled: true, - pattern: "I ate [0-9] pies", - rule_id: "pies", - }, - { - actions: [ - "notify", - { - set_tweak: "sound", - value: "default", - }, - { - set_tweak: "highlight", - }, - ], - enabled: true, - pattern: "b[!ai]ke", - rule_id: "bakebike", - }, ], override: [ { @@ -289,39 +244,6 @@ describe("NotificationService", function () { expect(actions.tweaks.highlight).toEqual(true); }); - // TODO: This is not spec compliant behaviour. - // - // See https://spec.matrix.org/v1.5/client-server-api/#conditions-1 which - // describes pattern should glob: - // - // 1. * matches 0 or more characters; - // 2. ? matches exactly one character - it("should bing on character group ([abc]) bing words.", function () { - testEvent.event.content!.body = "Ping!"; - let actions = pushProcessor.actionsForEvent(testEvent); - expect(actions.tweaks.highlight).toEqual(true); - testEvent.event.content!.body = "Pong!"; - actions = pushProcessor.actionsForEvent(testEvent); - expect(actions.tweaks.highlight).toEqual(true); - }); - - // TODO: This is not spec compliant behaviour. (See above.) - it("should bing on character range ([a-z]) bing words.", function () { - testEvent.event.content!.body = "I ate 6 pies"; - const actions = pushProcessor.actionsForEvent(testEvent); - expect(actions.tweaks.highlight).toEqual(true); - }); - - // TODO: This is not spec compliant behaviour. (See above.) - it("should bing on character negation ([!a]) bing words.", function () { - testEvent.event.content!.body = "boke"; - let actions = pushProcessor.actionsForEvent(testEvent); - expect(actions.tweaks.highlight).toEqual(true); - testEvent.event.content!.body = "bake"; - actions = pushProcessor.actionsForEvent(testEvent); - expect(actions.tweaks.highlight).toEqual(false); - }); - it("should not bing on room server ACL changes", function () { testEvent = utils.mkEvent({ type: EventType.RoomServerAcl, diff --git a/spec/unit/receipt-accumulator.spec.ts b/spec/unit/receipt-accumulator.spec.ts index 5c9e779de00..6e2c71cea83 100644 --- a/spec/unit/receipt-accumulator.spec.ts +++ b/spec/unit/receipt-accumulator.spec.ts @@ -129,6 +129,50 @@ describe("ReceiptAccumulator", function () { ]), ); }); + + it("Keeps main thread receipts even when an unthreaded receipt came later", () => { + const acc = new ReceiptAccumulator(); + + // Given receipts for the special thread "main" and also unthreaded + // receipts (which have no thread id). + const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1, "main"); + const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2); + + // When we collect them + acc.consumeEphemeralEvents([receipt1, receipt2]); + const newEvent = acc.buildAccumulatedReceiptEvent(roomId); + + // We preserve both: thread:main and unthreaded receipts are different + // things, with different meanings. + expect(newEvent).toEqual( + newMultiReceipt([ + ["$event1", ReceiptType.Read, "@alice:localhost", 1, "main"], + ["$event2", ReceiptType.Read, "@alice:localhost", 2, undefined], + ]), + ); + }); + + it("Keeps unthreaded receipts even when a main thread receipt came later", () => { + const acc = new ReceiptAccumulator(); + + // Given receipts for the special thread "main" and also unthreaded + // receipts (which have no thread id). + const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1); + const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2, "main"); + + // When we collect them + acc.consumeEphemeralEvents([receipt1, receipt2]); + const newEvent = acc.buildAccumulatedReceiptEvent(roomId); + + // We preserve both: thread:main and unthreaded receipts are different + // things, with different meanings. + expect(newEvent).toEqual( + newMultiReceipt([ + ["$event1", ReceiptType.Read, "@alice:localhost", 1, undefined], + ["$event2", ReceiptType.Read, "@alice:localhost", 2, "main"], + ]), + ); + }); }); const newReceipt = ( diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 8caca90bee8..2ed44dffd7b 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -51,7 +51,7 @@ import { TestClient } from "../TestClient"; import { ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts"; import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread"; import { Crypto } from "../../src/crypto"; -import { mkThread } from "../test-utils/thread"; +import * as threadUtils from "../test-utils/thread"; import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client"; import { logger } from "../../src/logger"; import { IMessageOpts } from "../test-utils/test-utils"; @@ -137,24 +137,6 @@ describe("Room", function () { room.client, ); - const mkReaction = (target: MatrixEvent) => - utils.mkEvent( - { - event: true, - type: EventType.Reaction, - user: userA, - room: roomId, - content: { - "m.relates_to": { - rel_type: RelationType.Annotation, - event_id: target.getId()!, - key: Math.random().toString(), - }, - }, - }, - room.client, - ); - const mkRedaction = (target: MatrixEvent) => utils.mkEvent( { @@ -168,30 +150,45 @@ describe("Room", function () { room.client, ); - const addRoomMainAndThreadMessages = ( - room: Room, - tsMain?: number, - tsThread?: number, - ): { mainEvent?: MatrixEvent; threadEvent?: MatrixEvent } => { - const result: { mainEvent?: MatrixEvent; threadEvent?: MatrixEvent } = {}; - - if (tsMain) { - result.mainEvent = mkMessage({ ts: tsMain }); - room.addLiveEvents([result.mainEvent]); - } + /** + * @see threadUtils.mkThread + */ + const mkThread = ( + opts: Partial[0]>, + ): ReturnType => { + return threadUtils.mkThread({ + room, + client: new TestClient().client, + authorId: "@bob:example.org", + participantUserIds: ["@bob:example.org"], + ...opts, + }); + }; - if (tsThread) { - const { rootEvent, thread } = mkThread({ - room, - client: new TestClient().client, - authorId: "@bob:example.org", - participantUserIds: ["@bob:example.org"], - }); - result.threadEvent = mkThreadResponse(rootEvent, { ts: tsThread }); - thread.liveTimeline.addEvent(result.threadEvent, { toStartOfTimeline: true }); - } + /** + * Creates a message and adds it to the end of the main live timeline. + * + * @param room - Room to add the message to + * @param timestamp - Timestamp of the message + * @return The message event + */ + const mkMessageInRoom = async (room: Room, timestamp: number) => { + const message = mkMessage({ ts: timestamp }); + await room.addLiveEvents([message]); + return message; + }; - return result; + /** + * Creates a message in a thread and adds it to the end of the thread live timeline. + * + * @param thread - Thread to add the message to + * @param timestamp - Timestamp of the message + * @returns The thread message event + */ + const mkMessageInThread = (thread: Thread, timestamp: number) => { + const message = mkThreadResponse(thread.rootEvent!, { ts: timestamp }); + thread.liveTimeline.addEvent(message, { toStartOfTimeline: false }); + return message; }; const addRoomThreads = ( @@ -202,24 +199,14 @@ describe("Room", function () { const result: { thread1?: Thread; thread2?: Thread } = {}; if (thread1EventTs !== null) { - const { rootEvent: thread1RootEvent, thread: thread1 } = mkThread({ - room, - client: new TestClient().client, - authorId: "@bob:example.org", - participantUserIds: ["@bob:example.org"], - }); + const { rootEvent: thread1RootEvent, thread: thread1 } = mkThread({ room }); const thread1Event = mkThreadResponse(thread1RootEvent, { ts: thread1EventTs }); thread1.liveTimeline.addEvent(thread1Event, { toStartOfTimeline: true }); result.thread1 = thread1; } if (thread2EventTs !== null) { - const { rootEvent: thread2RootEvent, thread: thread2 } = mkThread({ - room, - client: new TestClient().client, - authorId: "@bob:example.org", - participantUserIds: ["@bob:example.org"], - }); + const { rootEvent: thread2RootEvent, thread: thread2 } = mkThread({ room }); const thread2Event = mkThreadResponse(thread2RootEvent, { ts: thread2EventTs }); thread2.liveTimeline.addEvent(thread2Event, { toStartOfTimeline: true }); result.thread2 = thread2; @@ -332,23 +319,25 @@ 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("Make sure legacy overload passing options directly as parameters still works", async () => { + await expect(room.addLiveEvents(events, DuplicateStrategy.Replace, false)).resolves.not.toThrow(); + await expect(room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).resolves.not.toThrow(); + await expect( + // @ts-ignore + room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false), + ).rejects.toThrow(); }); - it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function () { - expect(function () { + it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", async function () { + return expect( // @ts-ignore room.addLiveEvents(events, { duplicateStrategy: "foo", - }); - }).toThrow(); + }), + ).rejects.toThrow(); }); - it("should replace a timeline event if dupe strategy is 'replace'", function () { + it("should replace a timeline event if dupe strategy is 'replace'", async function () { // make a duplicate const dupe = utils.mkMessage({ room: roomId, @@ -357,15 +346,15 @@ describe("Room", function () { event: true, }); dupe.event.event_id = events[0].getId(); - room.addLiveEvents(events); + await room.addLiveEvents(events); expect(room.timeline[0]).toEqual(events[0]); - room.addLiveEvents([dupe], { + await room.addLiveEvents([dupe], { duplicateStrategy: DuplicateStrategy.Replace, }); expect(room.timeline[0]).toEqual(dupe); }); - it("should ignore a given dupe event if dupe strategy is 'ignore'", function () { + it("should ignore a given dupe event if dupe strategy is 'ignore'", async function () { // make a duplicate const dupe = utils.mkMessage({ room: roomId, @@ -374,16 +363,16 @@ describe("Room", function () { event: true, }); dupe.event.event_id = events[0].getId(); - room.addLiveEvents(events); + await room.addLiveEvents(events); expect(room.timeline[0]).toEqual(events[0]); // @ts-ignore - room.addLiveEvents([dupe], { + await room.addLiveEvents([dupe], { duplicateStrategy: "ignore", }); expect(room.timeline[0]).toEqual(events[0]); }); - it("should emit 'Room.timeline' events", function () { + it("should emit 'Room.timeline' events", async function () { let callCount = 0; room.on(RoomEvent.Timeline, function (event, emitRoom, toStart) { callCount += 1; @@ -392,11 +381,11 @@ describe("Room", function () { expect(emitRoom).toEqual(room); expect(toStart).toBeFalsy(); }); - room.addLiveEvents(events); + await room.addLiveEvents(events); expect(callCount).toEqual(2); }); - it("should call setStateEvents on the right RoomState with the right forwardLooking value for new events", function () { + it("should call setStateEvents on the right RoomState with the right forwardLooking value for new events", async function () { const events: MatrixEvent[] = [ utils.mkMembership({ room: roomId, @@ -415,7 +404,7 @@ describe("Room", function () { }, }), ]; - room.addLiveEvents(events); + await room.addLiveEvents(events); expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[0]], { timelineWasEmpty: false }); expect(room.currentState.setStateEvents).toHaveBeenCalledWith([events[1]], { timelineWasEmpty: false }); expect(events[0].forwardLooking).toBe(true); @@ -423,7 +412,7 @@ describe("Room", function () { expect(room.oldState.setStateEvents).not.toHaveBeenCalled(); }); - it("should synthesize read receipts for the senders of events", function () { + it("should synthesize read receipts for the senders of events", async function () { const sentinel = { userId: userA, membership: "join", @@ -435,11 +424,11 @@ describe("Room", function () { } return null; }); - room.addLiveEvents(events); + await room.addLiveEvents(events); expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId()); }); - it("should emit Room.localEchoUpdated when a local echo is updated", function () { + it("should emit Room.localEchoUpdated when a local echo is updated", async function () { const localEvent = utils.mkMessage({ room: roomId, user: userA, @@ -470,7 +459,7 @@ describe("Room", function () { expect(stub.mock.calls[0][3]).toBeUndefined(); // then the remoteEvent - room.addLiveEvents([remoteEvent]); + await room.addLiveEvents([remoteEvent]); expect(room.timeline.length).toEqual(1); expect(stub).toHaveBeenCalledTimes(2); @@ -482,7 +471,7 @@ describe("Room", function () { expect(stub.mock.calls[1][3]).toBe(EventStatus.SENDING); }); - it("should be able to update local echo without a txn ID (/send then /sync)", function () { + it("should be able to update local echo without a txn ID (/send then /sync)", async function () { const eventJson = utils.mkMessage({ room: roomId, user: userA, @@ -508,14 +497,14 @@ describe("Room", function () { // then /sync returns the remoteEvent, it should de-dupe based on the event ID. const remoteEvent = new MatrixEvent(Object.assign({ event_id: realEventId }, eventJson)); expect(remoteEvent.getTxnId()).toBeUndefined(); - room.addLiveEvents([remoteEvent]); + await room.addLiveEvents([remoteEvent]); // the duplicate strategy code should ensure we don't add a 2nd event to the live timeline expect(room.timeline.length).toEqual(1); // but without the event ID matching we will still have the local event in pending events expect(room.getEventForTxnId(txnId)).toBeUndefined(); }); - it("should be able to update local echo without a txn ID (/sync then /send)", function () { + it("should be able to update local echo without a txn ID (/sync then /send)", async function () { const eventJson = utils.mkMessage({ room: roomId, user: userA, @@ -538,7 +527,7 @@ describe("Room", function () { const realEventId = "$real-event-id"; const remoteEvent = new MatrixEvent(Object.assign({ event_id: realEventId }, eventJson)); expect(remoteEvent.getUnsigned().transaction_id).toBeUndefined(); - room.addLiveEvents([remoteEvent]); + await room.addLiveEvents([remoteEvent]); expect(room.timeline.length).toEqual(2); // impossible to de-dupe as no txn ID or matching event ID // then the /send request returns the real event ID. @@ -551,7 +540,7 @@ describe("Room", function () { expect(room.getEventForTxnId(txnId)).toBeUndefined(); }); - it("should correctly handle remote echoes from other devices", () => { + it("should correctly handle remote echoes from other devices", async () => { const remoteEvent = utils.mkMessage({ room: roomId, user: userA, @@ -560,7 +549,7 @@ describe("Room", function () { remoteEvent.event.unsigned = { transaction_id: "TXN_ID" }; // add the remoteEvent - room.addLiveEvents([remoteEvent]); + await room.addLiveEvents([remoteEvent]); expect(room.timeline.length).toEqual(1); }); }); @@ -625,7 +614,7 @@ describe("Room", function () { }); describe("event metadata handling", function () { - it("should set event.sender for new and old events", function () { + it("should set event.sender for new and old events", async function () { const sentinel = { userId: userA, membership: "join", @@ -663,13 +652,13 @@ describe("Room", function () { event: true, content: { name: "Old Room Name" }, }); - room.addLiveEvents([newEv]); + await room.addLiveEvents([newEv]); expect(newEv.sender).toEqual(sentinel); room.addEventsToTimeline([oldEv], true, room.getLiveTimeline()); expect(oldEv.sender).toEqual(oldSentinel); }); - it("should set event.target for new and old m.room.member events", function () { + it("should set event.target for new and old m.room.member events", async function () { const sentinel = { userId: userA, membership: "join", @@ -707,7 +696,7 @@ describe("Room", function () { skey: userA, event: true, }); - room.addLiveEvents([newEv]); + await room.addLiveEvents([newEv]); expect(newEv.target).toEqual(sentinel); room.addEventsToTimeline([oldEv], true, room.getLiveTimeline()); expect(oldEv.target).toEqual(oldSentinel); @@ -776,12 +765,12 @@ describe("Room", function () { ]; }); - it("should copy state from previous timeline", function () { - room.addLiveEvents([events[0], events[1]]); + it("should copy state from previous timeline", async function () { + await room.addLiveEvents([events[0], events[1]]); expect(room.getLiveTimeline().getEvents().length).toEqual(2); room.resetLiveTimeline("sometoken", "someothertoken"); - room.addLiveEvents([events[2]]); + await room.addLiveEvents([events[2]]); const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS); const newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS); expect(room.getLiveTimeline().getEvents().length).toEqual(1); @@ -789,8 +778,8 @@ describe("Room", function () { expect(newState?.getStateEvents(EventType.RoomName, "")).toEqual(events[2]); }); - it("should reset the legacy timeline fields", function () { - room.addLiveEvents([events[0], events[1]]); + it("should reset the legacy timeline fields", async function () { + await room.addLiveEvents([events[0], events[1]]); expect(room.timeline.length).toEqual(2); const oldStateBeforeRunningReset = room.oldState; @@ -811,7 +800,7 @@ describe("Room", function () { room.resetLiveTimeline("sometoken", "someothertoken"); - room.addLiveEvents([events[2]]); + await room.addLiveEvents([events[2]]); const newLiveTimeline = room.getLiveTimeline(); expect(room.timeline).toEqual(newLiveTimeline.getEvents()); expect(room.oldState).toEqual(newLiveTimeline.getState(EventTimeline.BACKWARDS)); @@ -837,8 +826,8 @@ describe("Room", function () { expect(callCount).toEqual(1); }); - it("should " + (timelineSupport ? "remember" : "forget") + " old timelines", function () { - room.addLiveEvents([events[0]]); + it("should " + (timelineSupport ? "remember" : "forget") + " old timelines", async function () { + await room.addLiveEvents([events[0]]); expect(room.timeline.length).toEqual(1); const firstLiveTimeline = room.getLiveTimeline(); room.resetLiveTimeline("sometoken", "someothertoken"); @@ -881,8 +870,8 @@ describe("Room", function () { }), ]; - it("should handle events in the same timeline", function () { - room.addLiveEvents(events); + it("should handle events in the same timeline", async function () { + await room.addLiveEvents(events); expect( room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!), @@ -895,13 +884,13 @@ describe("Room", function () { ).toEqual(0); }); - it("should handle events in adjacent timelines", function () { + it("should handle events in adjacent timelines", async function () { const oldTimeline = room.addTimeline(); oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), Direction.Forward); room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, Direction.Backward); room.addEventsToTimeline([events[0]], false, oldTimeline); - room.addLiveEvents([events[1]]); + await room.addLiveEvents([events[1]]); expect( room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!), @@ -911,11 +900,11 @@ describe("Room", function () { ).toBeGreaterThan(0); }); - it("should return null for events in non-adjacent timelines", function () { + it("should return null for events in non-adjacent timelines", async function () { const oldTimeline = room.addTimeline(); room.addEventsToTimeline([events[0]], false, oldTimeline); - room.addLiveEvents([events[1]]); + await room.addLiveEvents([events[1]]); expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!)).toBe( null, @@ -925,8 +914,8 @@ describe("Room", function () { ); }); - it("should return null for unknown events", function () { - room.addLiveEvents(events); + it("should return null for unknown events", async function () { + await room.addLiveEvents(events); expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, "xxx")).toBe(null); expect(room.getUnfilteredTimelineSet().compareEventOrdering("xxx", events[0].getId()!)).toBe(null); @@ -1003,8 +992,8 @@ describe("Room", function () { }); describe("recalculate", function () { - const setJoinRule = function (rule: JoinRule) { - room.addLiveEvents([ + const setJoinRule = async function (rule: JoinRule) { + await room.addLiveEvents([ utils.mkEvent({ type: EventType.RoomJoinRules, room: roomId, @@ -1016,8 +1005,8 @@ describe("Room", function () { }), ]); }; - const setAltAliases = function (aliases: string[]) { - room.addLiveEvents([ + const setAltAliases = async function (aliases: string[]) { + await room.addLiveEvents([ utils.mkEvent({ type: EventType.RoomCanonicalAlias, room: roomId, @@ -1029,8 +1018,8 @@ describe("Room", function () { }), ]); }; - const setAlias = function (alias: string) { - room.addLiveEvents([ + const setAlias = async function (alias: string) { + await room.addLiveEvents([ utils.mkEvent({ type: EventType.RoomCanonicalAlias, room: roomId, @@ -1040,8 +1029,8 @@ describe("Room", function () { }), ]); }; - const setRoomName = function (name: string) { - room.addLiveEvents([ + const setRoomName = async function (name: string) { + await room.addLiveEvents([ utils.mkEvent({ type: EventType.RoomName, room: roomId, @@ -1053,14 +1042,14 @@ describe("Room", function () { }), ]); }; - const addMember = function (userId: string, state = "join", opts: any = {}) { + const addMember = async function (userId: string, state = "join", opts: any = {}) { opts.room = roomId; opts.mship = state; opts.user = opts.user || userId; opts.skey = userId; opts.event = true; const event = utils.mkMembership(opts); - room.addLiveEvents([event]); + await room.addLiveEvents([event]); return event; }; @@ -1072,10 +1061,10 @@ describe("Room", function () { describe("Room.recalculate => Stripped State Events", function () { it( "should set stripped state events as actual state events if the " + "room is an invite room", - function () { + async function () { const roomName = "flibble"; - const event = addMember(userA, "invite"); + const event = await addMember(userA, "invite"); event.event.unsigned = {}; event.event.unsigned.invite_room_state = [ { @@ -1093,8 +1082,8 @@ describe("Room", function () { }, ); - it("should not clobber state events if it isn't an invite room", function () { - const event = addMember(userA, "join"); + it("should not clobber state events if it isn't an invite room", async function () { + const event = await addMember(userA, "join"); const roomName = "flibble"; setRoomName(roomName); const roomNameToIgnore = "ignoreme"; @@ -1550,7 +1539,7 @@ describe("Room", function () { ]); }); - it("should prioritise the most recent event", function () { + it("should prioritise the most recent event", async function () { const events: MatrixEvent[] = [ utils.mkMessage({ room: roomId, @@ -1572,7 +1561,7 @@ describe("Room", function () { }), ]; - room.addLiveEvents(events); + await room.addLiveEvents(events); const ts = 13787898424; // check it initialises correctly @@ -1588,7 +1577,7 @@ describe("Room", function () { expect(room.getEventReadUpTo(userB)).toEqual(events[2].getId()); }); - it("should prioritise the most recent event even if it is synthetic", () => { + it("should prioritise the most recent event even if it is synthetic", async () => { const events: MatrixEvent[] = [ utils.mkMessage({ room: roomId, @@ -1610,7 +1599,7 @@ describe("Room", function () { }), ]; - room.addLiveEvents(events); + await room.addLiveEvents(events); const ts = 13787898424; // check it initialises correctly @@ -1686,66 +1675,72 @@ describe("Room", function () { }); describe("addPendingEvent", function () { - it("should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'", function () { - const client = new TestClient("@alice:example.com", "alicedevice").client; - client.supportsThreads = () => true; - const room = new Room(roomId, client, userA, { - pendingEventOrdering: PendingEventOrdering.Detached, - }); - const eventA = utils.mkMessage({ - room: roomId, - user: userA, - msg: "remote 1", - event: true, - }); - const eventB = utils.mkMessage({ - room: roomId, - user: userA, - msg: "local 1", - event: true, - }); - eventB.status = EventStatus.SENDING; - const eventC = utils.mkMessage({ - room: roomId, - user: userA, - msg: "remote 2", - event: true, - }); - room.addLiveEvents([eventA]); - room.addPendingEvent(eventB, "TXN1"); - room.addLiveEvents([eventC]); - expect(room.timeline).toEqual([eventA, eventC]); - expect(room.getPendingEvents()).toEqual([eventB]); - }); + it( + "should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'", + async function () { + const client = new TestClient("@alice:example.com", "alicedevice").client; + client.supportsThreads = () => true; + const room = new Room(roomId, client, userA, { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + const eventA = utils.mkMessage({ + room: roomId, + user: userA, + msg: "remote 1", + event: true, + }); + const eventB = utils.mkMessage({ + room: roomId, + user: userA, + msg: "local 1", + event: true, + }); + eventB.status = EventStatus.SENDING; + const eventC = utils.mkMessage({ + room: roomId, + user: userA, + msg: "remote 2", + event: true, + }); + await room.addLiveEvents([eventA]); + room.addPendingEvent(eventB, "TXN1"); + await room.addLiveEvents([eventC]); + expect(room.timeline).toEqual([eventA, eventC]); + expect(room.getPendingEvents()).toEqual([eventB]); + }, + ); - it("should add pending events to the timeline if " + "pendingEventOrdering == 'chronological'", function () { - const room = new Room(roomId, new TestClient(userA).client, userA, { - pendingEventOrdering: PendingEventOrdering.Chronological, - }); - const eventA = utils.mkMessage({ - room: roomId, - user: userA, - msg: "remote 1", - event: true, - }); - const eventB = utils.mkMessage({ - room: roomId, - user: userA, - msg: "local 1", - event: true, - }); - eventB.status = EventStatus.SENDING; - const eventC = utils.mkMessage({ - room: roomId, - user: userA, - msg: "remote 2", - event: true, - }); - room.addLiveEvents([eventA]); - room.addPendingEvent(eventB, "TXN1"); - room.addLiveEvents([eventC]); - expect(room.timeline).toEqual([eventA, eventB, eventC]); - }); + it( + "should add pending events to the timeline if " + "pendingEventOrdering == 'chronological'", + async function () { + const room = new Room(roomId, new TestClient(userA).client, userA, { + pendingEventOrdering: PendingEventOrdering.Chronological, + }); + const eventA = utils.mkMessage({ + room: roomId, + user: userA, + msg: "remote 1", + event: true, + }); + const eventB = utils.mkMessage({ + room: roomId, + user: userA, + msg: "local 1", + event: true, + }); + eventB.status = EventStatus.SENDING; + const eventC = utils.mkMessage({ + room: roomId, + user: userA, + msg: "remote 2", + event: true, + }); + await room.addLiveEvents([eventA]); + room.addPendingEvent(eventB, "TXN1"); + await room.addLiveEvents([eventC]); + expect(room.timeline).toEqual([eventA, eventB, eventC]); + }, + ); it("should apply redactions eagerly in the pending event list", () => { const client = new TestClient("@alice:example.com", "alicedevice").client; @@ -1932,13 +1927,7 @@ describe("Room", function () { it("should allow retry on error", async function () { const client = createClientMock(new Error("server says no")); const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true }); - let hasThrown = false; - try { - await room.loadMembersIfNeeded(); - } catch (err) { - hasThrown = true; - } - expect(hasThrown).toEqual(true); + await expect(room.loadMembersIfNeeded()).rejects.toBeTruthy(); client.members.mockReturnValue({ chunk: [memberEvent] }); await room.loadMembersIfNeeded(); @@ -2023,9 +2012,9 @@ describe("Room", function () { }); expect(room.guessDMUserId()).toEqual(userB); }); - it("should return first member that isn't self", function () { + it("should return first member that isn't self", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userB, mship: "join", @@ -2089,9 +2078,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("Empty room"); }); - it("should return a display name if one other member is in the room", function () { + it("should return a display name if one other member is in the room", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2110,9 +2099,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("User B"); }); - it("should return a display name if one other member is banned", function () { + it("should return a display name if one other member is banned", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2131,9 +2120,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)"); }); - it("should return a display name if one other member is invited", function () { + it("should return a display name if one other member is invited", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2152,9 +2141,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("User B"); }); - it("should return 'Empty room (was User B)' if User B left the room", function () { + it("should return 'Empty room (was User B)' if User B left the room", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2173,9 +2162,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)"); }); - it("should return 'User B and User C' if in a room with two other users", function () { + it("should return 'User B and User C' if in a room with two other users", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2201,9 +2190,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("User B and User C"); }); - it("should return 'User B and 2 others' if in a room with three other users", function () { + it("should return 'User B and 2 others' if in a room with three other users", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2238,9 +2227,9 @@ describe("Room", function () { }); describe("io.element.functional_users", function () { - it("should return a display name (default behaviour) if no one is marked as a functional member", function () { + it("should return a display name (default behaviour) if no one is marked as a functional member", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2268,9 +2257,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("User B"); }); - it("should return a display name (default behaviour) if service members is a number (invalid)", function () { + it("should return a display name (default behaviour) if service members is a number (invalid)", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2298,9 +2287,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("User B"); }); - it("should return a display name (default behaviour) if service members is a string (invalid)", function () { + it("should return a display name (default behaviour) if service members is a string (invalid)", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2328,9 +2317,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("User B"); }); - it("should return 'Empty room' if the only other member is a functional member", function () { + it("should return 'Empty room' if the only other member is a functional member", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2358,9 +2347,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("Empty room"); }); - it("should return 'User B' if User B is the only other member who isn't a functional member", function () { + it("should return 'User B' if User B is the only other member who isn't a functional member", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2396,9 +2385,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("User B"); }); - it("should return 'Empty room' if all other members are functional members", function () { + it("should return 'Empty room' if all other members are functional members", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2434,9 +2423,9 @@ describe("Room", function () { expect(room.getDefaultRoomName(userA)).toEqual("Empty room"); }); - it("should not break if an unjoined user is marked as a service user", function () { + it("should not break if an unjoined user is marked as a service user", async function () { const room = new Room(roomId, new TestClient(userA).client, userA); - room.addLiveEvents([ + await room.addLiveEvents([ utils.mkMembership({ user: userA, mship: "join", @@ -2510,12 +2499,7 @@ describe("Room", function () { }); it("returns the same model when creating a thread twice", () => { - const { thread, rootEvent } = mkThread({ - room, - client: new TestClient().client, - authorId: "@bob:example.org", - participantUserIds: ["@bob:example.org"], - }); + const { thread, rootEvent } = mkThread({ room }); expect(thread).toBeInstanceOf(Thread); @@ -2572,7 +2556,7 @@ describe("Room", function () { }); let prom = emitPromise(room, ThreadEvent.New); - room.addLiveEvents([randomMessage, threadRoot, threadResponse]); + await room.addLiveEvents([randomMessage, threadRoot, threadResponse]); const thread: Thread = await prom; await emitPromise(room, ThreadEvent.Update); @@ -2599,7 +2583,7 @@ describe("Room", function () { }); prom = emitPromise(room, ThreadEvent.Update); - room.addLiveEvents([threadResponseEdit]); + await room.addLiveEvents([threadResponseEdit]); await prom; expect(thread.replyToEvent!.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body); }); @@ -2630,7 +2614,7 @@ describe("Room", function () { }); let prom = emitPromise(room, ThreadEvent.New); - room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]); + await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]); const thread = await prom; await emitPromise(room, ThreadEvent.Update); @@ -2665,7 +2649,7 @@ describe("Room", function () { prom = emitPromise(thread, ThreadEvent.Update); const threadResponse1Redaction = mkRedaction(threadResponse1); - room.addLiveEvents([threadResponse1Redaction]); + await room.addLiveEvents([threadResponse1Redaction]); await prom; expect(thread).toHaveLength(1); expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId()); @@ -2680,7 +2664,7 @@ describe("Room", function () { threadResponse1.localTimestamp += 1000; const threadResponse2 = mkThreadResponse(threadRoot); threadResponse2.localTimestamp += 2000; - const threadResponse2Reaction = mkReaction(threadResponse2); + const threadResponse2Reaction = utils.mkReaction(threadResponse2, room.client, userA, roomId); room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({ @@ -2698,7 +2682,7 @@ describe("Room", function () { }); const prom = emitPromise(room, ThreadEvent.New); - room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]); + await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]); const thread = await prom; await emitPromise(room, ThreadEvent.Update); @@ -2706,7 +2690,7 @@ describe("Room", function () { expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId()); const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction); - room.addLiveEvents([threadResponse2ReactionRedaction]); + await room.addLiveEvents([threadResponse2ReactionRedaction]); expect(thread).toHaveLength(2); expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId()); }); @@ -2720,7 +2704,7 @@ describe("Room", function () { threadResponse1.localTimestamp += 1000; const threadResponse2 = mkThreadResponse(threadRoot); threadResponse2.localTimestamp += 2000; - const threadResponse2Reaction = mkReaction(threadResponse2); + const threadResponse2Reaction = utils.mkReaction(threadResponse2, room.client, userA, roomId); room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({ @@ -2738,7 +2722,7 @@ describe("Room", function () { }); let prom = emitPromise(room, ThreadEvent.New); - room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]); + await room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]); const thread = await prom; await emitPromise(room, ThreadEvent.Update); @@ -2747,7 +2731,7 @@ describe("Room", function () { prom = emitPromise(room, ThreadEvent.Update); const threadRootRedaction = mkRedaction(threadRoot); - room.addLiveEvents([threadRootRedaction]); + await room.addLiveEvents([threadRootRedaction]); await prom; expect(thread).toHaveLength(2); }); @@ -2800,12 +2784,12 @@ describe("Room", function () { }); let prom = emitPromise(room, ThreadEvent.New); - room.addLiveEvents([threadRoot, threadResponse1]); + await room.addLiveEvents([threadRoot, threadResponse1]); const thread: Thread = await prom; await emitPromise(room, ThreadEvent.Update); expect(thread.initialEventsFetched).toBeTruthy(); - room.addLiveEvents([threadResponse2]); + await room.addLiveEvents([threadResponse2]); expect(thread).toHaveLength(2); expect(thread.replyToEvent!.getId()).toBe(threadResponse2.getId()); @@ -2826,7 +2810,7 @@ describe("Room", function () { prom = emitPromise(room, ThreadEvent.Update); const threadResponse2Redaction = mkRedaction(threadResponse2); - room.addLiveEvents([threadResponse2Redaction]); + await room.addLiveEvents([threadResponse2Redaction]); await prom; await emitPromise(room, ThreadEvent.Update); expect(thread).toHaveLength(1); @@ -2850,7 +2834,7 @@ describe("Room", function () { prom = emitPromise(room, ThreadEvent.Delete); const prom2 = emitPromise(room, RoomEvent.Timeline); const threadResponse1Redaction = mkRedaction(threadResponse1); - room.addLiveEvents([threadResponse1Redaction]); + await room.addLiveEvents([threadResponse1Redaction]); await prom; await prom2; expect(thread).toHaveLength(0); @@ -2868,8 +2852,8 @@ describe("Room", function () { const randomMessage = mkMessage(); const threadRoot = mkMessage(); const threadResponse1 = mkThreadResponse(threadRoot); - const threadReaction1 = mkReaction(threadRoot); - const threadReaction2 = mkReaction(threadRoot); + const threadReaction1 = utils.mkReaction(threadRoot, room.client, userA, roomId); + const threadReaction2 = utils.mkReaction(threadRoot, room.client, userA, roomId); const threadReaction2Redaction = mkRedaction(threadReaction2); const roots = new Set([threadRoot.getId()!]); @@ -2906,8 +2890,8 @@ describe("Room", function () { it("thread response and its relations&redactions should be only in thread timeline", () => { const threadRoot = mkMessage(); const threadResponse1 = mkThreadResponse(threadRoot); - const threadReaction1 = mkReaction(threadResponse1); - const threadReaction2 = mkReaction(threadResponse1); + const threadReaction1 = utils.mkReaction(threadResponse1, room.client, userA, roomId); + const threadReaction2 = utils.mkReaction(threadResponse1, room.client, userA, roomId); const threadReaction2Redaction = mkRedaction(threadReaction2); const roots = new Set([threadRoot.getId()!]); @@ -2928,8 +2912,8 @@ describe("Room", function () { const threadRoot = mkMessage(); const threadResponse1 = mkThreadResponse(threadRoot); const reply1 = mkReply(threadResponse1); - const reaction1 = mkReaction(reply1); - const reaction2 = mkReaction(reply1); + const reaction1 = utils.mkReaction(reply1, room.client, userA, roomId); + const reaction2 = utils.mkReaction(reply1, room.client, userA, roomId); const reaction2Redaction = mkRedaction(reply1); const roots = new Set([threadRoot.getId()!]); @@ -2963,14 +2947,14 @@ describe("Room", function () { it("should aggregate relations in thread event timeline set", async () => { Thread.setServerSideSupport(FeatureSupport.Stable); const threadRoot = mkMessage(); - const rootReaction = mkReaction(threadRoot); + const rootReaction = utils.mkReaction(threadRoot, room.client, userA, roomId); const threadResponse = mkThreadResponse(threadRoot); - const threadReaction = mkReaction(threadResponse); + const threadReaction = utils.mkReaction(threadResponse, room.client, userA, roomId); const events = [threadRoot, rootReaction, threadResponse, threadReaction]; const prom = emitPromise(room, ThreadEvent.New); - room.addLiveEvents(events); + await room.addLiveEvents(events); const thread = await prom; expect(thread).toBe(threadRoot.getThread()); expect(thread.rootEvent).toBe(threadRoot); @@ -3476,21 +3460,21 @@ describe("Room", function () { expect(room.findPredecessor()).toBeNull(); }); - it("Returns null if the create event has no predecessor", () => { + it("Returns null if the create event has no predecessor", async () => { const room = new Room("roomid", client!, "@u:example.com"); - room.addLiveEvents([roomCreateEvent("roomid", null)]); + await room.addLiveEvents([roomCreateEvent("roomid", null)]); expect(room.findPredecessor()).toBeNull(); }); - it("Returns the predecessor ID if one is provided via create event", () => { + it("Returns the predecessor ID if one is provided via create event", async () => { const room = new Room("roomid", client!, "@u:example.com"); - room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]); + await room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]); expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" }); }); - it("Prefers the m.predecessor event if one exists", () => { + it("Prefers the m.predecessor event if one exists", async () => { const room = new Room("roomid", client!, "@u:example.com"); - room.addLiveEvents([ + await room.addLiveEvents([ roomCreateEvent("roomid", "replacedroomid"), predecessorEvent("roomid", "otherreplacedroomid"), ]); @@ -3502,9 +3486,9 @@ describe("Room", function () { }); }); - it("uses the m.predecessor event ID if provided", () => { + it("uses the m.predecessor event ID if provided", async () => { const room = new Room("roomid", client!, "@u:example.com"); - room.addLiveEvents([ + await room.addLiveEvents([ roomCreateEvent("roomid", "replacedroomid"), predecessorEvent("roomid", "otherreplacedroomid", "lstevtid", ["one.example.com", "two.example.com"]), ]); @@ -3516,9 +3500,9 @@ describe("Room", function () { }); }); - it("Ignores the m.predecessor event if we don't ask to use it", () => { + it("Ignores the m.predecessor event if we don't ask to use it", async () => { const room = new Room("roomid", client!, "@u:example.com"); - room.addLiveEvents([ + await room.addLiveEvents([ roomCreateEvent("roomid", "replacedroomid"), predecessorEvent("roomid", "otherreplacedroomid"), ]); @@ -3527,9 +3511,9 @@ describe("Room", function () { expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" }); }); - it("Ignores the m.predecessor event and returns null if we don't ask to use it", () => { + it("Ignores the m.predecessor event and returns null if we don't ask to use it", async () => { const room = new Room("roomid", client!, "@u:example.com"); - room.addLiveEvents([ + await room.addLiveEvents([ roomCreateEvent("roomid", null), // Create event has no predecessor predecessorEvent("roomid", "otherreplacedroomid", "lastevtid"), ]); @@ -3540,32 +3524,50 @@ describe("Room", function () { }); describe("getLastLiveEvent", () => { - let lastEventInMainTimeline: MatrixEvent; - let lastEventInThread: MatrixEvent; - it("when there are no events, it should return undefined", () => { expect(room.getLastLiveEvent()).toBeUndefined(); }); - it("when there is only an event in the main timeline and there are no threads, it should return the last event from the main timeline", () => { - lastEventInMainTimeline = addRoomMainAndThreadMessages(room, 23).mainEvent!; - room.addLiveEvents([lastEventInMainTimeline]); + it("when there is only an event in the main timeline and there are no threads, it should return the last event from the main timeline", async () => { + const lastEventInMainTimeline = await mkMessageInRoom(room, 23); expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline); }); + /** + * This should normally not happen. The test exists only for the sake of completeness. + * No event is added to the room's live timeline here. + */ it("when there is no event in the room live timeline but in a thread, it should return the last event from the thread", () => { - lastEventInThread = addRoomMainAndThreadMessages(room, undefined, 42).threadEvent!; + const { thread } = mkThread({ room, length: 0 }); + const lastEventInThread = mkMessageInThread(thread, 42); expect(room.getLastLiveEvent()).toBe(lastEventInThread); }); describe("when there are events in both, the main timeline and threads", () => { - it("and the last event is in a thread, it should return the last event from the thread", () => { - lastEventInThread = addRoomMainAndThreadMessages(room, 23, 42).threadEvent!; + it("and the last event is in a thread, it should return the last event from the thread", async () => { + await mkMessageInRoom(room, 23); + const { thread } = mkThread({ room, length: 0 }); + const lastEventInThread = mkMessageInThread(thread, 42); + expect(room.getLastLiveEvent()).toBe(lastEventInThread); + }); + + it("and the last event is in the main timeline, it should return the last event from the main timeline", async () => { + const lastEventInMainTimeline = await mkMessageInRoom(room, 42); + const { thread } = mkThread({ room, length: 0 }); + mkMessageInThread(thread, 23); + expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline); + }); + + it("and both events have the same timestamp, it should return the last event from the thread", async () => { + await mkMessageInRoom(room, 23); + const { thread } = mkThread({ room, length: 0 }); + const lastEventInThread = mkMessageInThread(thread, 23); expect(room.getLastLiveEvent()).toBe(lastEventInThread); }); - it("and the last event is in the main timeline, it should return the last event from the main timeline", () => { - lastEventInMainTimeline = addRoomMainAndThreadMessages(room, 42, 23).mainEvent!; + it("and there is a thread without any messages, it should return the last event from the main timeline", async () => { + const lastEventInMainTimeline = await mkMessageInRoom(room, 23); + mkThread({ room, length: 0 }); expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline); }); }); diff --git a/spec/unit/rust-crypto/CrossSigningIdentity.spec.ts b/spec/unit/rust-crypto/CrossSigningIdentity.spec.ts new file mode 100644 index 00000000000..d0fb8bd36be --- /dev/null +++ b/spec/unit/rust-crypto/CrossSigningIdentity.spec.ts @@ -0,0 +1,79 @@ +/* +Copyright 2023 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 { Mocked } from "jest-mock"; +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; + +import { CrossSigningIdentity } from "../../../src/rust-crypto/CrossSigningIdentity"; +import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; + +describe("CrossSigningIdentity", () => { + describe("bootstrapCrossSigning", () => { + /** the CrossSigningIdentity implementation under test */ + let crossSigning: CrossSigningIdentity; + + /** a mocked-up OlmMachine which crossSigning is connected to */ + let olmMachine: Mocked; + + /** A mock OutgoingRequestProcessor which crossSigning is connected to */ + let outgoingRequestProcessor: Mocked; + + beforeEach(async () => { + await RustSdkCryptoJs.initAsync(); + + olmMachine = { + crossSigningStatus: jest.fn(), + bootstrapCrossSigning: jest.fn(), + close: jest.fn(), + } as unknown as Mocked; + + outgoingRequestProcessor = { + makeOutgoingRequest: jest.fn(), + } as unknown as Mocked; + + crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor); + }); + + it("should do nothing if keys are present on-device and in secret storage", async () => { + olmMachine.crossSigningStatus.mockResolvedValue({ + hasMaster: true, + hasSelfSigning: true, + hasUserSigning: true, + }); + // TODO: secret storage + await crossSigning.bootstrapCrossSigning({}); + expect(olmMachine.bootstrapCrossSigning).not.toHaveBeenCalled(); + expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled(); + }); + + it("should call bootstrapCrossSigning if a reset is forced", async () => { + olmMachine.bootstrapCrossSigning.mockResolvedValue([]); + await crossSigning.bootstrapCrossSigning({ setupNewCrossSigning: true }); + expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true); + }); + + it("should call bootstrapCrossSigning if we need new keys", async () => { + olmMachine.crossSigningStatus.mockResolvedValue({ + hasMaster: false, + hasSelfSigning: false, + hasUserSigning: false, + }); + olmMachine.bootstrapCrossSigning.mockResolvedValue([]); + await crossSigning.bootstrapCrossSigning({}); + expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true); + }); + }); +}); diff --git a/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts b/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts index 28d1e9b3508..bb1cdff6ab1 100644 --- a/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts +++ b/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts @@ -24,11 +24,12 @@ import { KeysUploadRequest, RoomMessageRequest, SignatureUploadRequest, + SigningKeysUploadRequest, ToDeviceRequest, } from "@matrix-org/matrix-sdk-crypto-js"; import { TypedEventEmitter } from "../../../src/models/typed-event-emitter"; -import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src"; +import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, UIAuthCallback } from "../../../src"; import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; describe("OutgoingRequestProcessor", () => { @@ -80,6 +81,12 @@ describe("OutgoingRequestProcessor", () => { "https://example.com/_matrix/client/v3/keys/signatures/upload", ], ["KeysBackupRequest", KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"], + [ + "SigningKeysUploadRequest", + SigningKeysUploadRequest, + "POST", + "https://example.com/_matrix/client/v3/keys/device_signing/upload", + ], ]; test.each(tests)(`should handle %ss`, async (_, RequestClass, expectedMethod, expectedPath) => { @@ -171,6 +178,40 @@ describe("OutgoingRequestProcessor", () => { httpBackend.verifyNoOutstandingRequests(); }); + it("should handle SigningKeysUploadRequests with UIA", async () => { + // first, mock up a request as we might expect to receive it from the Rust layer ... + const testReq = { foo: "bar" }; + const outgoingRequest = new SigningKeysUploadRequest("1234", JSON.stringify(testReq)); + + // also create a UIA callback + const authCallback: UIAuthCallback = async (makeRequest) => { + return await makeRequest({ type: "test" }); + }; + + // ... then poke the request into the OutgoingRequestProcessor under test + const reqProm = processor.makeOutgoingRequest(outgoingRequest, authCallback); + + // Now: check that it makes a matching HTTP request ... + const testResponse = '{"result":1}'; + httpBackend + .when("POST", "/_matrix") + .check((req) => { + expect(req.path).toEqual("https://example.com/_matrix/client/v3/keys/device_signing/upload"); + expect(JSON.parse(req.rawData)).toEqual({ foo: "bar", auth: { type: "test" } }); + expect(req.headers["Accept"]).toEqual("application/json"); + expect(req.headers["Content-Type"]).toEqual("application/json"); + }) + .respond(200, testResponse, true); + + // ... and that it calls OlmMachine.markAsSent. + const markSentCallPromise = awaitCallToMarkAsSent(); + await httpBackend.flushAllExpected(); + + await Promise.all([reqProm, markSentCallPromise]); + expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse); + httpBackend.verifyNoOutstandingRequests(); + }); + it("does not explode with unknown requests", async () => { const outgoingRequest = { id: "5678", type: 987 }; const markSentCallPromise = awaitCallToMarkAsSent(); diff --git a/spec/unit/rust-crypto/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts index c31aa15f2fc..71df210ad97 100644 --- a/spec/unit/rust-crypto/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -22,11 +22,12 @@ import { Mocked } from "jest-mock"; import { RustCrypto } from "../../../src/rust-crypto/rust-crypto"; import { initRustCrypto } from "../../../src/rust-crypto"; -import { IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src"; +import { IHttpOpts, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src"; import { mkEvent } from "../../test-utils/test-utils"; import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend"; import { IEventDecryptionResult } from "../../../src/@types/crypto"; import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; +import { ServerSideSecretStorage } from "../../../src/secret-storage"; afterEach(() => { // reset fake-indexeddb after each test, to make sure we don't leak connections @@ -35,16 +36,15 @@ afterEach(() => { indexedDB = new IDBFactory(); }); -describe("RustCrypto", () => { - const TEST_USER = "@alice:example.com"; - const TEST_DEVICE_ID = "TEST_DEVICE"; +const TEST_USER = "@alice:example.com"; +const TEST_DEVICE_ID = "TEST_DEVICE"; +describe("RustCrypto", () => { describe(".exportRoomKeys", () => { let rustCrypto: RustCrypto; beforeEach(async () => { - const mockHttpApi = {} as MatrixClient["http"]; - rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto; + rustCrypto = await makeTestRustCrypto(); }); it("should return a list", async () => { @@ -57,8 +57,7 @@ describe("RustCrypto", () => { let rustCrypto: RustCrypto; beforeEach(async () => { - const mockHttpApi = {} as MatrixClient["http"]; - rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto; + rustCrypto = await makeTestRustCrypto(); }); it("should pass through unencrypted to-device messages", async () => { @@ -94,6 +93,32 @@ describe("RustCrypto", () => { }); }); + it("isCrossSigningReady", async () => { + const rustCrypto = await makeTestRustCrypto(); + await expect(rustCrypto.isCrossSigningReady()).resolves.toBe(false); + }); + + it("getCrossSigningKeyId", async () => { + const rustCrypto = await makeTestRustCrypto(); + await expect(rustCrypto.getCrossSigningKeyId()).resolves.toBe(null); + }); + + it("bootstrapCrossSigning delegates to CrossSigningIdentity", async () => { + const rustCrypto = await makeTestRustCrypto(); + const mockCrossSigningIdentity = { + bootstrapCrossSigning: jest.fn().mockResolvedValue(undefined), + }; + // @ts-ignore private property + rustCrypto.crossSigningIdentity = mockCrossSigningIdentity; + await rustCrypto.bootstrapCrossSigning({}); + expect(mockCrossSigningIdentity.bootstrapCrossSigning).toHaveBeenCalledWith({}); + }); + + it("isSecretStorageReady", async () => { + const rustCrypto = await makeTestRustCrypto(); + await expect(rustCrypto.isSecretStorageReady()).resolves.toBe(false); + }); + describe("outgoing requests", () => { /** the RustCrypto implementation under test */ let rustCrypto: RustCrypto; @@ -141,7 +166,13 @@ describe("RustCrypto", () => { makeOutgoingRequest: jest.fn(), } as unknown as Mocked; - rustCrypto = new RustCrypto(olmMachine, {} as MatrixHttpApi, TEST_USER, TEST_DEVICE_ID); + rustCrypto = new RustCrypto( + olmMachine, + {} as MatrixHttpApi, + TEST_USER, + TEST_DEVICE_ID, + {} as ServerSideSecretStorage, + ); rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor; }); @@ -206,8 +237,7 @@ describe("RustCrypto", () => { let rustCrypto: RustCrypto; beforeEach(async () => { - const mockHttpApi = {} as MatrixClient["http"]; - rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto; + rustCrypto = await makeTestRustCrypto(); }); it("should handle unencrypted events", () => { @@ -235,7 +265,7 @@ describe("RustCrypto", () => { let rustCrypto: RustCrypto; beforeEach(async () => { - rustCrypto = await initRustCrypto({} as MatrixClient["http"], TEST_USER, TEST_DEVICE_ID); + rustCrypto = await makeTestRustCrypto(); }); it("should be true by default", () => { @@ -258,7 +288,13 @@ describe("RustCrypto", () => { olmMachine = { getDevice: jest.fn(), } as unknown as Mocked; - rustCrypto = new RustCrypto(olmMachine, {} as MatrixClient["http"], TEST_USER, TEST_DEVICE_ID); + rustCrypto = new RustCrypto( + olmMachine, + {} as MatrixClient["http"], + TEST_USER, + TEST_DEVICE_ID, + {} as ServerSideSecretStorage, + ); }); it("should call getDevice", async () => { @@ -282,3 +318,16 @@ describe("RustCrypto", () => { }); }); }); + +/** build a basic RustCrypto instance for testing + * + * just provides default arguments for initRustCrypto() + */ +async function makeTestRustCrypto( + http: MatrixHttpApi = {} as MatrixClient["http"], + userId: string = TEST_USER, + deviceId: string = TEST_DEVICE_ID, + secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage, +): Promise { + return await initRustCrypto(http, userId, deviceId, secretStorage); +} diff --git a/spec/unit/stores/indexeddb.spec.ts b/spec/unit/stores/indexeddb.spec.ts index 544a3d75c5b..7f2bc25d458 100644 --- a/spec/unit/stores/indexeddb.spec.ts +++ b/spec/unit/stores/indexeddb.spec.ts @@ -254,4 +254,31 @@ describe("IndexedDBStore", () => { }); await expect(store.startup()).rejects.toThrow("Test"); }); + + it("remote worker should terminate upon destroy call", async () => { + const terminate = jest.fn(); + const worker = new (class MockWorker { + private onmessage!: (data: any) => void; + postMessage(data: any) { + this.onmessage({ + data: { + command: "cmd_success", + seq: data.seq, + result: [], + }, + }); + } + public terminate = terminate; + })() as unknown as Worker; + + const store = new IndexedDBStore({ + indexedDB: indexedDB, + dbName: "database", + localStorage, + workerFactory: () => worker, + }); + await store.startup(); + await expect(store.destroy()).resolves; + expect(terminate).toHaveBeenCalled(); + }); }); diff --git a/spec/unit/sync-accumulator.spec.ts b/spec/unit/sync-accumulator.spec.ts index d434e0145ac..e257aa960c3 100644 --- a/spec/unit/sync-accumulator.spec.ts +++ b/spec/unit/sync-accumulator.spec.ts @@ -545,6 +545,25 @@ describe("SyncAccumulator", function () { expect(summary["m.heroes"]).toEqual(["@bob:bar"]); }); + it("should correctly update summary properties to zero", function () { + // When we receive updates of a summary property, the last of which is 0 + sa.accumulate( + createSyncResponseWithSummary({ + "m.heroes": ["@alice:bar"], + "m.invited_member_count": 2, + }), + ); + sa.accumulate( + createSyncResponseWithSummary({ + "m.heroes": ["@alice:bar"], + "m.invited_member_count": 0, + }), + ); + const summary = sa.getJSON().roomsData.join["!foo:bar"].summary; + // Then we give an answer of 0 + expect(summary["m.invited_member_count"]).toEqual(0); + }); + it("should return correctly adjusted age attributes", () => { const delta = 1000; const startingTs = 1000; diff --git a/spec/unit/utils.spec.ts b/spec/unit/utils.spec.ts index 13fffb93cd0..5c2db9e9c88 100644 --- a/spec/unit/utils.spec.ts +++ b/spec/unit/utils.spec.ts @@ -30,6 +30,8 @@ import { sortEventsByLatestContentTimestamp, safeSet, MapWithDefault, + globToRegexp, + escapeRegExp, } from "../../src/utils"; import { logger } from "../../src/logger"; import { mkMessage } from "../test-utils/test-utils"; @@ -725,4 +727,19 @@ describe("utils", function () { await utils.immediate(); }); }); + + describe("escapeRegExp", () => { + it("should escape XYZ", () => { + expect(escapeRegExp("[FIT-Connect Zustelldienst \\(Testumgebung\\)]")).toMatchInlineSnapshot( + `"\\[FIT-Connect Zustelldienst \\\\\\(Testumgebung\\\\\\)\\]"`, + ); + }); + }); + + describe("globToRegexp", () => { + it("should not explode when given regexes as globs", () => { + const result = globToRegexp("[FIT-Connect Zustelldienst \\(Testumgebung\\)]"); + expect(result).toMatchInlineSnapshot(`"\\[FIT-Connect Zustelldienst \\\\\\(Testumgebung\\\\\\)\\]"`); + }); + }); }); diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index db84ee54004..5a3b384539f 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -25,6 +25,7 @@ import { CallType, CallState, CallParty, + CallDirection, } from "../../../src/webrtc/call"; import { MCallAnswer, @@ -1053,14 +1054,7 @@ describe("Call", function () { mockSendEvent.mockReset(); - let caught = false; - try { - call.reject(); - } catch (e) { - caught = true; - } - - expect(caught).toEqual(true); + expect(() => call.reject()).toThrow(); expect(client.client.sendEvent).not.toHaveBeenCalled(); call.hangup(CallErrorCode.UserHangup, true); @@ -1652,12 +1646,18 @@ describe("Call", function () { 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!(); + jest.spyOn(mockPeerConn, "restartIce"); + }); + + it("should restart ICE gathering after being disconnected for 2 seconds", () => { + jest.advanceTimersByTime(3 * 1000); + expect(mockPeerConn.restartIce).toHaveBeenCalled(); }); it("should hang up after being disconnected for 30 seconds", () => { @@ -1665,6 +1665,20 @@ describe("Call", function () { expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false); }); + it("should restart ICE gathering once again after ICE being failed", () => { + mockPeerConn.iceConnectionState = "failed"; + mockPeerConn.iceConnectionStateChangeListener!(); + expect(mockPeerConn.restartIce).toHaveBeenCalled(); + }); + + it("should call hangup after ICE being failed and if there not exists a restartIce method", () => { + // @ts-ignore + mockPeerConn.restartIce = null; + mockPeerConn.iceConnectionState = "failed"; + mockPeerConn.iceConnectionStateChangeListener!(); + expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false); + }); + it("should not hangup if we've managed to re-connect", () => { mockPeerConn.iceConnectionState = "connected"; mockPeerConn.iceConnectionStateChangeListener!(); @@ -1692,4 +1706,110 @@ describe("Call", function () { expect(onReplace).toHaveBeenCalled(); }); }); + describe("should handle glare in negotiation process", () => { + beforeEach(async () => { + // cut methods not want to test + call.hangup = () => null; + call.isLocalOnHold = () => true; + // @ts-ignore + call.updateRemoteSDPStreamMetadata = jest.fn(); + // @ts-ignore + call.getRidOfRTXCodecs = jest.fn(); + // @ts-ignore + call.createAnswer = jest.fn().mockResolvedValue({}); + // @ts-ignore + call.sendVoipEvent = jest.fn(); + }); + + it("and reject remote offer if not polite and have pending local offer", async () => { + // not polite user == CallDirection.Outbound + call.direction = CallDirection.Outbound; + // have already a local offer + // @ts-ignore + call.makingOffer = true; + const offerEvent = makeMockEvent("@test:foo", { + description: { + type: "offer", + sdp: DUMMY_SDP, + }, + }); + // @ts-ignore + call.peerConn = { + signalingState: "have-local-offer", + setRemoteDescription: jest.fn(), + }; + await call.onNegotiateReceived(offerEvent); + expect(call.peerConn?.setRemoteDescription).not.toHaveBeenCalled(); + }); + + it("and not reject remote offer if not polite and do have pending answer", async () => { + // not polite user == CallDirection.Outbound + call.direction = CallDirection.Outbound; + // have not a local offer + // @ts-ignore + call.makingOffer = false; + + // If we have a setRemoteDescription() answer operation pending, then + // we will be "stable" by the time the next setRemoteDescription() is + // executed, so we count this being readyForOffer when deciding whether to + // ignore the offer. + // @ts-ignore + call.isSettingRemoteAnswerPending = true; + const offerEvent = makeMockEvent("@test:foo", { + description: { + type: "offer", + sdp: DUMMY_SDP, + }, + }); + // @ts-ignore + call.peerConn = { + signalingState: "have-local-offer", + setRemoteDescription: jest.fn(), + }; + await call.onNegotiateReceived(offerEvent); + expect(call.peerConn?.setRemoteDescription).toHaveBeenCalled(); + }); + + it("and not reject remote offer if not polite and do not have pending local offer", async () => { + // not polite user == CallDirection.Outbound + call.direction = CallDirection.Outbound; + // have no local offer + // @ts-ignore + call.makingOffer = false; + const offerEvent = makeMockEvent("@test:foo", { + description: { + type: "offer", + sdp: DUMMY_SDP, + }, + }); + // @ts-ignore + call.peerConn = { + signalingState: "stable", + setRemoteDescription: jest.fn(), + }; + await call.onNegotiateReceived(offerEvent); + expect(call.peerConn?.setRemoteDescription).toHaveBeenCalled(); + }); + + it("and if polite do rollback pending local offer", async () => { + // polite user == CallDirection.Inbound + call.direction = CallDirection.Inbound; + // have already a local offer + // @ts-ignore + call.makingOffer = true; + const offerEvent = makeMockEvent("@test:foo", { + description: { + type: "offer", + sdp: DUMMY_SDP, + }, + }); + // @ts-ignore + call.peerConn = { + signalingState: "have-local-offer", + setRemoteDescription: jest.fn(), + }; + await call.onNegotiateReceived(offerEvent); + expect(call.peerConn?.setRemoteDescription).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/unit/webrtc/groupCall.spec.ts b/spec/unit/webrtc/groupCall.spec.ts index 4d839827b1b..876b658e2cb 100644 --- a/spec/unit/webrtc/groupCall.spec.ts +++ b/spec/unit/webrtc/groupCall.spec.ts @@ -186,10 +186,7 @@ describe("Group Call", function () { it("sets state to local call feed uninitialized when getUserMedia() fails", async () => { jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValue("Error"); - try { - await groupCall.initLocalCallFeed(); - } catch (e) {} - + await expect(groupCall.initLocalCallFeed()).rejects.toBeTruthy(); expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized); }); @@ -517,8 +514,7 @@ describe("Group Call", function () { await groupCall.setMicrophoneMuted(false); expect(groupCall.isMicrophoneMuted()).toEqual(false); - jest.advanceTimersByTime(groupCall.pttMaxTransmitTime + 100); - + await jest.advanceTimersByTimeAsync(groupCall.pttMaxTransmitTime + 100); expect(groupCall.isMicrophoneMuted()).toEqual(true); }); @@ -585,7 +581,15 @@ describe("Group Call", function () { }); mockCall.sendMetadataUpdate = jest.fn().mockReturnValue(metadataUpdatePromise); + const getUserMediaStreamFlush = Promise.resolve("stream"); + // @ts-ignore + mockCall.cleint = { + getMediaHandler: { + getUserMediaStream: jest.fn().mockReturnValue(getUserMediaStreamFlush), + }, + }; const mutePromise = groupCall.setMicrophoneMuted(true); + await getUserMediaStreamFlush; // we should be muted at this point, before the metadata update has been sent expect(groupCall.isMicrophoneMuted()).toEqual(true); expect(mockCall.localUsermediaFeed.setAudioVideoMuted).toHaveBeenCalled(); @@ -892,14 +896,34 @@ describe("Group Call", function () { expect(await groupCall.setMicrophoneMuted(false)).toBe(false); }); - it("returns false when no permission for audio stream", async () => { + it("returns false when no permission for audio stream and localCallFeed do not have an audio track", async () => { const groupCall = await createAndEnterGroupCall(mockClient, room); + // @ts-ignore + jest.spyOn(groupCall.localCallFeed, "hasAudioTrack", "get").mockReturnValue(false); jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce( new Error("No Permission"), ); expect(await groupCall.setMicrophoneMuted(false)).toBe(false); }); + it("returns false when user media stream null", async () => { + const groupCall = await createAndEnterGroupCall(mockClient, room); + // @ts-ignore + jest.spyOn(groupCall.localCallFeed, "hasAudioTrack", "get").mockReturnValue(false); + // @ts-ignore + jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockResolvedValue({} as MediaStream); + expect(await groupCall.setMicrophoneMuted(false)).toBe(false); + }); + + it("returns true when no permission for audio stream but localCallFeed has a audio track already", async () => { + const groupCall = await createAndEnterGroupCall(mockClient, room); + // @ts-ignore + jest.spyOn(groupCall.localCallFeed, "hasAudioTrack", "get").mockReturnValue(true); + jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream"); + expect(mockClient.getMediaHandler().getUserMediaStream).not.toHaveBeenCalled(); + expect(await groupCall.setMicrophoneMuted(false)).toBe(true); + }); + it("returns false when unmuting video with no video device", async () => { const groupCall = await createAndEnterGroupCall(mockClient, room); jest.spyOn(mockClient.getMediaHandler(), "hasVideoDevice").mockResolvedValue(false); diff --git a/spec/unit/webrtc/stats/callStatsReportGatherer.spec.ts b/spec/unit/webrtc/stats/callStatsReportGatherer.spec.ts new file mode 100644 index 00000000000..e6a364d6a8b --- /dev/null +++ b/spec/unit/webrtc/stats/callStatsReportGatherer.spec.ts @@ -0,0 +1,189 @@ +/* +Copyright 2023 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 { CallStatsReportGatherer } from "../../../../src/webrtc/stats/callStatsReportGatherer"; +import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter"; +import { MediaSsrcHandler } from "../../../../src/webrtc/stats/media/mediaSsrcHandler"; + +const CALL_ID = "CALL_ID"; +const USER_ID = "USER_ID"; + +describe("CallStatsReportGatherer", () => { + let collector: CallStatsReportGatherer; + let rtcSpy: RTCPeerConnection; + let emitter: StatsReportEmitter; + beforeEach(() => { + rtcSpy = { getStats: () => new Promise(() => null) } as RTCPeerConnection; + rtcSpy.addEventListener = jest.fn(); + emitter = new StatsReportEmitter(); + collector = new CallStatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter); + }); + + describe("on process stats", () => { + it("if active calculate stats reports", async () => { + const getStats = jest.spyOn(rtcSpy, "getStats"); + const report = {} as RTCStatsReport; + report.forEach = jest.fn().mockReturnValue([]); + getStats.mockResolvedValue(report); + const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID"); + expect(getStats).toHaveBeenCalled(); + expect(actual).toEqual({ + isFirstCollection: true, + receivedMedia: 0, + receivedAudioMedia: 0, + receivedVideoMedia: 0, + audioTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }); + expect(collector.getActive()).toBeTruthy(); + }); + + it("if not active do not calculate stats reports", async () => { + collector.setActive(false); + const getStats = jest.spyOn(rtcSpy, "getStats"); + await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID"); + expect(getStats).not.toHaveBeenCalled(); + }); + + it("if get reports fails, the collector becomes inactive", async () => { + expect(collector.getActive()).toBeTruthy(); + const getStats = jest.spyOn(rtcSpy, "getStats"); + getStats.mockRejectedValue(new Error("unknown")); + await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID"); + expect(getStats).toHaveBeenCalled(); + expect(collector.getActive()).toBeFalsy(); + }); + + it("if active and getStats returns not an RTCStatsReport inside a promise the collector fails and becomes inactive", async () => { + const getStats = jest.spyOn(rtcSpy, "getStats"); + // @ts-ignore + getStats.mockReturnValue({}); + const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID"); + expect(actual).toEqual({ + isFirstCollection: true, + receivedMedia: 0, + receivedAudioMedia: 0, + receivedVideoMedia: 0, + audioTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }); + expect(getStats).toHaveBeenCalled(); + expect(collector.getActive()).toBeFalsy(); + }); + + it("if active and the collector runs not the first time the Summery Stats is marked as not fits collection", async () => { + const getStats = jest.spyOn(rtcSpy, "getStats"); + // @ts-ignore + collector.previousStatsReport = {} as RTCStatsReport; + const report = {} as RTCStatsReport; + report.forEach = jest.fn().mockReturnValue([]); + getStats.mockResolvedValue(report); + const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID"); + expect(getStats).toHaveBeenCalled(); + expect(actual).toEqual({ + isFirstCollection: false, + receivedMedia: 0, + receivedAudioMedia: 0, + receivedVideoMedia: 0, + audioTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }); + expect(collector.getActive()).toBeTruthy(); + }); + }); + + describe("on signal state change event", () => { + let events: { [key: string]: any }; + beforeEach(() => { + events = []; + // Define the addEventListener method with a Jest mock function + rtcSpy.addEventListener = jest.fn((event: any, callback: any) => { + events[event] = callback; + }); + + collector = new CallStatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter); + }); + it("in case of stable, parse remote and local description", () => { + // @ts-ignore + const mediaSsrcHandler = { + parse: jest.fn(), + ssrcToMid: jest.fn(), + findMidBySsrc: jest.fn(), + getSsrcToMidMap: jest.fn(), + } as MediaSsrcHandler; + + const remoteSDP = "sdp"; + const localSDP = "sdp"; + + // @ts-ignore + rtcSpy.signalingState = "stable"; + + // @ts-ignore + rtcSpy.currentRemoteDescription = { sdp: remoteSDP }; + // @ts-ignore + rtcSpy.currentLocalDescription = { sdp: localSDP }; + + // @ts-ignore + collector.trackStats.mediaSsrcHandler = mediaSsrcHandler; + + events["signalingstatechange"](); + expect(mediaSsrcHandler.parse).toHaveBeenCalledWith(remoteSDP, "remote"); + expect(mediaSsrcHandler.parse).toHaveBeenCalledWith(localSDP, "local"); + }); + }); +}); diff --git a/spec/unit/webrtc/stats/statsReportBuilder.spec.ts b/spec/unit/webrtc/stats/connectionStatsReportBuilder.spec.ts similarity index 89% rename from spec/unit/webrtc/stats/statsReportBuilder.spec.ts rename to spec/unit/webrtc/stats/connectionStatsReportBuilder.spec.ts index fd75c90546d..d8fb9a756f9 100644 --- a/spec/unit/webrtc/stats/statsReportBuilder.spec.ts +++ b/spec/unit/webrtc/stats/connectionStatsReportBuilder.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import { TrackID } from "../../../../src/webrtc/stats/statsReport"; import { MediaTrackStats } from "../../../../src/webrtc/stats/media/mediaTrackStats"; -import { StatsReportBuilder } from "../../../../src/webrtc/stats/statsReportBuilder"; +import { ConnectionStatsReportBuilder } from "../../../../src/webrtc/stats/connectionStatsReportBuilder"; describe("StatsReportBuilder", () => { const LOCAL_VIDEO_TRACK_ID = "LOCAL_VIDEO_TRACK_ID"; @@ -39,7 +39,7 @@ describe("StatsReportBuilder", () => { describe("should build stats", () => { it("by media track stats.", async () => { - expect(StatsReportBuilder.build(stats)).toEqual({ + expect(ConnectionStatsReportBuilder.build(stats)).toEqual({ bitrate: { audio: { download: 4000, @@ -91,6 +91,13 @@ describe("StatsReportBuilder", () => { ["REMOTE_AUDIO_TRACK_ID", 0.1], ["REMOTE_VIDEO_TRACK_ID", 50], ]), + audioConcealment: new Map([ + ["REMOTE_AUDIO_TRACK_ID", { concealedAudio: 3000, totalAudioDuration: 3000 * 20 }], + ]), + totalAudioConcealment: { + concealedAudio: 3000, + totalAudioDuration: (1 / 0.05) * 3000, + }, }); }); }); @@ -104,6 +111,7 @@ describe("StatsReportBuilder", () => { remoteAudioTrack.setLoss({ packetsTotal: 20, packetsLost: 0, isDownloadStream: true }); remoteAudioTrack.setBitrate({ download: 4000, upload: 0 }); remoteAudioTrack.setJitter(0.1); + remoteAudioTrack.setAudioConcealment(3000, 3000 * 20); localVideoTrack.setCodec("v8"); localVideoTrack.setLoss({ packetsTotal: 30, packetsLost: 6, isDownloadStream: false }); diff --git a/spec/unit/webrtc/stats/connectionStatsReporter.spec.ts b/spec/unit/webrtc/stats/connectionStatsReporter.spec.ts index 1c9b2123319..7356f2576ab 100644 --- a/spec/unit/webrtc/stats/connectionStatsReporter.spec.ts +++ b/spec/unit/webrtc/stats/connectionStatsReporter.spec.ts @@ -13,7 +13,7 @@ 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 { ConnectionStatsReporter } from "../../../../src/webrtc/stats/connectionStatsReporter"; +import { ConnectionStatsBuilder } from "../../../../src/webrtc/stats/connectionStatsBuilder"; describe("ConnectionStatsReporter", () => { describe("should on bandwidth stats", () => { @@ -22,11 +22,11 @@ describe("ConnectionStatsReporter", () => { availableIncomingBitrate: 1000, availableOutgoingBitrate: 2000, } as RTCIceCandidatePairStats; - expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 1, upload: 2 }); + expect(ConnectionStatsBuilder.buildBandwidthReport(stats)).toEqual({ download: 1, upload: 2 }); }); it("build empty bandwidth report if chromium starts attributes not available", () => { const stats = {} as RTCIceCandidatePairStats; - expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 0, upload: 0 }); + expect(ConnectionStatsBuilder.buildBandwidthReport(stats)).toEqual({ download: 0, upload: 0 }); }); }); @@ -36,11 +36,11 @@ describe("ConnectionStatsReporter", () => { availableIncomingBitrate: 1000, availableOutgoingBitrate: 2000, } as RTCIceCandidatePairStats; - expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 1, upload: 2 }); + expect(ConnectionStatsBuilder.buildBandwidthReport(stats)).toEqual({ download: 1, upload: 2 }); }); it("build empty bandwidth report if chromium starts attributes not available", () => { const stats = {} as RTCIceCandidatePairStats; - expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 0, upload: 0 }); + expect(ConnectionStatsBuilder.buildBandwidthReport(stats)).toEqual({ download: 0, upload: 0 }); }); }); }); diff --git a/spec/unit/webrtc/stats/groupCallStats.spec.ts b/spec/unit/webrtc/stats/groupCallStats.spec.ts index f0b6c436546..6aa45f307f9 100644 --- a/spec/unit/webrtc/stats/groupCallStats.spec.ts +++ b/spec/unit/webrtc/stats/groupCallStats.spec.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import { GroupCallStats } from "../../../../src/webrtc/stats/groupCallStats"; -import { SummaryStats } from "../../../../src/webrtc/stats/summaryStats"; +import { CallStatsReportSummary } from "../../../../src/webrtc/stats/callStatsReportSummary"; const GROUP_CALL_ID = "GROUP_ID"; const LOCAL_USER_ID = "LOCAL_USER_ID"; @@ -92,12 +92,27 @@ describe("GroupCallStats", () => { const collector = stats.getStatsReportGatherer("CALL_ID"); stats.reports.emitSummaryStatsReport = jest.fn(); const summaryStats = { + isFirstCollection: true, receivedMedia: 0, receivedAudioMedia: 0, receivedVideoMedia: 0, - audioTrackSummary: { count: 0, muted: 0 }, - videoTrackSummary: { count: 0, muted: 0 }, - } as SummaryStats; + audioTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + } as CallStatsReportSummary; let processStatsSpy; if (collector) { processStatsSpy = jest.spyOn(collector, "processStats").mockResolvedValue(summaryStats); diff --git a/spec/unit/webrtc/stats/statsReportGatherer.spec.ts b/spec/unit/webrtc/stats/statsReportGatherer.spec.ts deleted file mode 100644 index aad12b4a36a..00000000000 --- a/spec/unit/webrtc/stats/statsReportGatherer.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2023 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 { StatsReportGatherer } from "../../../../src/webrtc/stats/statsReportGatherer"; -import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter"; - -const CALL_ID = "CALL_ID"; -const USER_ID = "USER_ID"; - -describe("StatsReportGatherer", () => { - let collector: StatsReportGatherer; - let rtcSpy: RTCPeerConnection; - let emitter: StatsReportEmitter; - beforeEach(() => { - rtcSpy = { getStats: () => new Promise(() => null) } as RTCPeerConnection; - rtcSpy.addEventListener = jest.fn(); - emitter = new StatsReportEmitter(); - collector = new StatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter); - }); - - describe("on process stats", () => { - it("if active calculate stats reports", async () => { - const getStats = jest.spyOn(rtcSpy, "getStats"); - const report = {} as RTCStatsReport; - report.forEach = jest.fn().mockReturnValue([]); - getStats.mockResolvedValue(report); - const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID"); - expect(getStats).toHaveBeenCalled(); - expect(actual).toEqual({ - receivedMedia: 0, - receivedAudioMedia: 0, - receivedVideoMedia: 0, - audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }); - expect(collector.getActive()).toBeTruthy(); - }); - - it("if not active do not calculate stats reports", async () => { - collector.setActive(false); - const getStats = jest.spyOn(rtcSpy, "getStats"); - await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID"); - expect(getStats).not.toHaveBeenCalled(); - }); - - it("if get reports fails, the collector becomes inactive", async () => { - expect(collector.getActive()).toBeTruthy(); - const getStats = jest.spyOn(rtcSpy, "getStats"); - getStats.mockRejectedValue(new Error("unknown")); - await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID"); - expect(getStats).toHaveBeenCalled(); - expect(collector.getActive()).toBeFalsy(); - }); - - it("if active an RTCStatsReport not a promise the collector becomes inactive", async () => { - const getStats = jest.spyOn(rtcSpy, "getStats"); - // @ts-ignore - getStats.mockReturnValue({}); - const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID"); - expect(actual).toEqual({ - receivedMedia: 0, - receivedAudioMedia: 0, - receivedVideoMedia: 0, - audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }); - expect(getStats).toHaveBeenCalled(); - expect(collector.getActive()).toBeFalsy(); - }); - }); -}); diff --git a/spec/unit/webrtc/stats/summaryStatsReportGatherer.spec.ts b/spec/unit/webrtc/stats/summaryStatsReportGatherer.spec.ts new file mode 100644 index 00000000000..eefff0d9b49 --- /dev/null +++ b/spec/unit/webrtc/stats/summaryStatsReportGatherer.spec.ts @@ -0,0 +1,592 @@ +/* +Copyright 2023 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 { SummaryStatsReportGatherer } from "../../../../src/webrtc/stats/summaryStatsReportGatherer"; +import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter"; + +describe("SummaryStatsReportGatherer", () => { + let reporter: SummaryStatsReportGatherer; + let emitter: StatsReportEmitter; + beforeEach(() => { + emitter = new StatsReportEmitter(); + emitter.emitSummaryStatsReport = jest.fn(); + reporter = new SummaryStatsReportGatherer(emitter); + }); + + describe("build Summary Stats Report", () => { + it("should do nothing if summary list empty", async () => { + reporter.build([]); + expect(emitter.emitSummaryStatsReport).not.toHaveBeenCalled(); + }); + it("should do nothing if a summary stats element collection the is first time", async () => { + reporter.build([ + { + isFirstCollection: true, + receivedMedia: 10, + receivedAudioMedia: 4, + receivedVideoMedia: 6, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 100, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + ]); + expect(emitter.emitSummaryStatsReport).not.toHaveBeenCalled(); + }); + + it("should trigger new summary report", async () => { + const summary = [ + { + isFirstCollection: false, + receivedMedia: 10, + receivedAudioMedia: 4, + receivedVideoMedia: 6, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 100, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + { + isFirstCollection: false, + receivedMedia: 13, + receivedAudioMedia: 0, + receivedVideoMedia: 13, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 5, + totalAudio: 100, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + { + isFirstCollection: false, + receivedMedia: 0, + receivedAudioMedia: 0, + receivedVideoMedia: 0, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 10, + totalAudio: 100, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + { + isFirstCollection: false, + receivedMedia: 15, + receivedAudioMedia: 6, + receivedVideoMedia: 9, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 100, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + ]; + reporter.build(summary); + expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ + percentageReceivedMedia: 0.5, + percentageReceivedAudioMedia: 0.5, + percentageReceivedVideoMedia: 0.75, + maxJitter: 0, + maxPacketLoss: 0, + peerConnections: 4, + percentageConcealedAudio: 0.0375, + }); + }); + + it("as received video Media, although video was not received, but because video muted", async () => { + const summary = [ + { + isFirstCollection: false, + receivedMedia: 10, + receivedAudioMedia: 10, + receivedVideoMedia: 0, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 1, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + ]; + reporter.build(summary); + expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ + percentageReceivedMedia: 1, + percentageReceivedAudioMedia: 1, + percentageReceivedVideoMedia: 1, + maxJitter: 0, + maxPacketLoss: 0, + peerConnections: 1, + percentageConcealedAudio: 0, + }); + }); + + it("as received no video Media, because only on video was muted", async () => { + const summary = [ + { + isFirstCollection: false, + receivedMedia: 10, + receivedAudioMedia: 10, + receivedVideoMedia: 0, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 2, + muted: 1, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + ]; + reporter.build(summary); + expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ + percentageReceivedMedia: 0, + percentageReceivedAudioMedia: 1, + percentageReceivedVideoMedia: 0, + maxJitter: 0, + maxPacketLoss: 0, + peerConnections: 1, + percentageConcealedAudio: 0, + }); + }); + + it("as received no audio Media, although audio not received and audio muted", async () => { + const summary = [ + { + isFirstCollection: false, + receivedMedia: 100, + receivedAudioMedia: 0, + receivedVideoMedia: 100, + audioTrackSummary: { + count: 1, + muted: 1, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + ]; + reporter.build(summary); + expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ + percentageReceivedMedia: 0, + percentageReceivedAudioMedia: 0, + percentageReceivedVideoMedia: 1, + maxJitter: 0, + maxPacketLoss: 0, + peerConnections: 1, + percentageConcealedAudio: 0, + }); + }); + + it("should find max jitter and max packet loss", async () => { + const summary = [ + { + isFirstCollection: false, + receivedMedia: 1, + receivedAudioMedia: 1, + receivedVideoMedia: 1, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + { + isFirstCollection: false, + receivedMedia: 1, + receivedAudioMedia: 1, + receivedVideoMedia: 1, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 20, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + { + isFirstCollection: false, + receivedMedia: 1, + receivedAudioMedia: 1, + receivedVideoMedia: 1, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 2, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 2, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, + }, + { + isFirstCollection: false, + receivedMedia: 1, + receivedAudioMedia: 1, + receivedVideoMedia: 1, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 2, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 40, + concealedAudio: 0, + totalAudio: 0, + }, + }, + ]; + reporter.build(summary); + expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ + percentageReceivedMedia: 1, + percentageReceivedAudioMedia: 1, + percentageReceivedVideoMedia: 1, + maxJitter: 20, + maxPacketLoss: 40, + peerConnections: 4, + percentageConcealedAudio: 0, + }); + }); + + it("as received video Media, if no audio track received should count as received Media", async () => { + const summary = [ + { + isFirstCollection: false, + receivedMedia: 10, + receivedAudioMedia: 0, + receivedVideoMedia: 10, + audioTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + ]; + reporter.build(summary); + expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ + percentageReceivedMedia: 1, + percentageReceivedAudioMedia: 1, + percentageReceivedVideoMedia: 1, + maxJitter: 0, + maxPacketLoss: 0, + peerConnections: 1, + percentageConcealedAudio: 0, + }); + }); + + it("as received audio Media, if no video track received should count as received Media", async () => { + const summary = [ + { + isFirstCollection: false, + receivedMedia: 1, + receivedAudioMedia: 22, + receivedVideoMedia: 0, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + ]; + reporter.build(summary); + expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ + percentageReceivedMedia: 1, + percentageReceivedAudioMedia: 1, + percentageReceivedVideoMedia: 1, + maxJitter: 0, + maxPacketLoss: 0, + peerConnections: 1, + percentageConcealedAudio: 0, + }); + }); + + it("as received no media at all, as received Media", async () => { + const summary = [ + { + isFirstCollection: false, + receivedMedia: 0, + receivedAudioMedia: 0, + receivedVideoMedia: 0, + audioTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + ]; + reporter.build(summary); + expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ + percentageReceivedMedia: 1, + percentageReceivedAudioMedia: 1, + percentageReceivedVideoMedia: 1, + maxJitter: 0, + maxPacketLoss: 0, + peerConnections: 1, + percentageConcealedAudio: 0, + }); + }); + + it("should filter the first time summery stats", async () => { + const summary = [ + { + isFirstCollection: false, + receivedMedia: 1, + receivedAudioMedia: 1, + receivedVideoMedia: 1, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + { + isFirstCollection: true, + receivedMedia: 1, + receivedAudioMedia: 1, + receivedVideoMedia: 1, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 20, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }, + }, + { + isFirstCollection: false, + receivedMedia: 1, + receivedAudioMedia: 1, + receivedVideoMedia: 1, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 2, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 2, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, + }, + { + isFirstCollection: false, + receivedMedia: 1, + receivedAudioMedia: 1, + receivedVideoMedia: 1, + audioTrackSummary: { + count: 1, + muted: 0, + maxJitter: 2, + maxPacketLoss: 5, + concealedAudio: 0, + totalAudio: 0, + }, + videoTrackSummary: { + count: 1, + muted: 0, + maxJitter: 0, + maxPacketLoss: 40, + concealedAudio: 0, + totalAudio: 0, + }, + }, + ]; + reporter.build(summary); + expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ + percentageReceivedMedia: 1, + percentageReceivedAudioMedia: 1, + percentageReceivedVideoMedia: 1, + maxJitter: 2, + maxPacketLoss: 40, + peerConnections: 3, + percentageConcealedAudio: 0, + }); + }); + }); +}); diff --git a/spec/unit/webrtc/stats/summaryStatsReporter.spec.ts b/spec/unit/webrtc/stats/summaryStatsReporter.spec.ts deleted file mode 100644 index 2692dbf03eb..00000000000 --- a/spec/unit/webrtc/stats/summaryStatsReporter.spec.ts +++ /dev/null @@ -1,236 +0,0 @@ -/* -Copyright 2023 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 { SummaryStatsReporter } from "../../../../src/webrtc/stats/summaryStatsReporter"; -import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter"; - -describe("SummaryStatsReporter", () => { - let reporter: SummaryStatsReporter; - let emitter: StatsReportEmitter; - beforeEach(() => { - emitter = new StatsReportEmitter(); - emitter.emitSummaryStatsReport = jest.fn(); - reporter = new SummaryStatsReporter(emitter); - }); - - describe("build Summary Stats Report", () => { - it("should do nothing if summary list empty", async () => { - reporter.build([]); - expect(emitter.emitSummaryStatsReport).not.toHaveBeenCalled(); - }); - - it("should trigger new summary report", async () => { - const summary = [ - { - receivedMedia: 10, - receivedAudioMedia: 4, - receivedVideoMedia: 6, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }, - { - receivedMedia: 13, - receivedAudioMedia: 0, - receivedVideoMedia: 13, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }, - { - receivedMedia: 0, - receivedAudioMedia: 0, - receivedVideoMedia: 0, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }, - { - receivedMedia: 15, - receivedAudioMedia: 6, - receivedVideoMedia: 9, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }, - ]; - reporter.build(summary); - expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ - percentageReceivedMedia: 0.5, - percentageReceivedAudioMedia: 0.5, - percentageReceivedVideoMedia: 0.75, - maxJitter: 0, - maxPacketLoss: 0, - }); - }); - - it("as received video Media, although video was not received, but because video muted", async () => { - const summary = [ - { - receivedMedia: 10, - receivedAudioMedia: 10, - receivedVideoMedia: 0, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 1, maxJitter: 0, maxPacketLoss: 0 }, - }, - ]; - reporter.build(summary); - expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ - percentageReceivedMedia: 1, - percentageReceivedAudioMedia: 1, - percentageReceivedVideoMedia: 1, - maxJitter: 0, - maxPacketLoss: 0, - }); - }); - - it("as received no video Media, because only on video was muted", async () => { - const summary = [ - { - receivedMedia: 10, - receivedAudioMedia: 10, - receivedVideoMedia: 0, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 2, muted: 1, maxJitter: 0, maxPacketLoss: 0 }, - }, - ]; - reporter.build(summary); - expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ - percentageReceivedMedia: 0, - percentageReceivedAudioMedia: 1, - percentageReceivedVideoMedia: 0, - maxJitter: 0, - maxPacketLoss: 0, - }); - }); - - it("as received no audio Media, although audio not received and audio muted", async () => { - const summary = [ - { - receivedMedia: 100, - receivedAudioMedia: 0, - receivedVideoMedia: 100, - audioTrackSummary: { count: 1, muted: 1, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }, - ]; - reporter.build(summary); - expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ - percentageReceivedMedia: 0, - percentageReceivedAudioMedia: 0, - percentageReceivedVideoMedia: 1, - maxJitter: 0, - maxPacketLoss: 0, - }); - }); - - it("should find max jitter and max packet loss", async () => { - const summary = [ - { - receivedMedia: 1, - receivedAudioMedia: 1, - receivedVideoMedia: 1, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }, - { - receivedMedia: 1, - receivedAudioMedia: 1, - receivedVideoMedia: 1, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 20, maxPacketLoss: 5 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }, - { - receivedMedia: 1, - receivedAudioMedia: 1, - receivedVideoMedia: 1, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 }, - }, - { - receivedMedia: 1, - receivedAudioMedia: 1, - receivedVideoMedia: 1, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 40 }, - }, - ]; - reporter.build(summary); - expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ - percentageReceivedMedia: 1, - percentageReceivedAudioMedia: 1, - percentageReceivedVideoMedia: 1, - maxJitter: 20, - maxPacketLoss: 40, - }); - }); - - it("as received video Media, if no audio track received should count as received Media", async () => { - const summary = [ - { - receivedMedia: 10, - receivedAudioMedia: 0, - receivedVideoMedia: 10, - audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }, - ]; - reporter.build(summary); - expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ - percentageReceivedMedia: 1, - percentageReceivedAudioMedia: 1, - percentageReceivedVideoMedia: 1, - maxJitter: 0, - maxPacketLoss: 0, - }); - }); - - it("as received audio Media, if no video track received should count as received Media", async () => { - const summary = [ - { - receivedMedia: 1, - receivedAudioMedia: 22, - receivedVideoMedia: 0, - audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }, - ]; - reporter.build(summary); - expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ - percentageReceivedMedia: 1, - percentageReceivedAudioMedia: 1, - percentageReceivedVideoMedia: 1, - maxJitter: 0, - maxPacketLoss: 0, - }); - }); - - it("as received no media at all, as received Media", async () => { - const summary = [ - { - receivedMedia: 0, - receivedAudioMedia: 0, - receivedVideoMedia: 0, - audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }, - }, - ]; - reporter.build(summary); - expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({ - percentageReceivedMedia: 1, - percentageReceivedAudioMedia: 1, - percentageReceivedVideoMedia: 1, - maxJitter: 0, - maxPacketLoss: 0, - }); - }); - }); -}); diff --git a/spec/unit/webrtc/stats/trackStatsReporter.spec.ts b/spec/unit/webrtc/stats/trackStatsBuilder.spec.ts similarity index 83% rename from spec/unit/webrtc/stats/trackStatsReporter.spec.ts rename to spec/unit/webrtc/stats/trackStatsBuilder.spec.ts index 79b1ad680aa..9bfe1169cdf 100644 --- a/spec/unit/webrtc/stats/trackStatsReporter.spec.ts +++ b/spec/unit/webrtc/stats/trackStatsBuilder.spec.ts @@ -13,20 +13,20 @@ 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 { TrackStatsReporter } from "../../../../src/webrtc/stats/trackStatsReporter"; +import { TrackStatsBuilder } from "../../../../src/webrtc/stats/trackStatsBuilder"; import { MediaTrackStats } from "../../../../src/webrtc/stats/media/mediaTrackStats"; -describe("TrackStatsReporter", () => { +describe("TrackStatsBuilder", () => { describe("should on frame and resolution stats", () => { it("creating empty frame and resolution report, if no data available.", async () => { const trackStats = new MediaTrackStats("1", "local", "video"); - TrackStatsReporter.buildFramerateResolution(trackStats, {}); + TrackStatsBuilder.buildFramerateResolution(trackStats, {}); expect(trackStats.getFramerate()).toEqual(0); expect(trackStats.getResolution()).toEqual({ width: -1, height: -1 }); }); it("creating empty frame and resolution report.", async () => { const trackStats = new MediaTrackStats("1", "remote", "video"); - TrackStatsReporter.buildFramerateResolution(trackStats, { + TrackStatsBuilder.buildFramerateResolution(trackStats, { framesPerSecond: 22.2, frameHeight: 180, frameWidth: 360, @@ -39,7 +39,7 @@ describe("TrackStatsReporter", () => { describe("should on simulcast", () => { it("creating simulcast framerate.", async () => { const trackStats = new MediaTrackStats("1", "local", "video"); - TrackStatsReporter.calculateSimulcastFramerate( + TrackStatsBuilder.calculateSimulcastFramerate( trackStats, { framesSent: 100, @@ -58,7 +58,7 @@ describe("TrackStatsReporter", () => { describe("should on bytes received stats", () => { it("creating build bitrate received report.", async () => { const trackStats = new MediaTrackStats("1", "remote", "video"); - TrackStatsReporter.buildBitrateReceived( + TrackStatsBuilder.buildBitrateReceived( trackStats, { bytesReceived: 2001000, @@ -73,7 +73,7 @@ describe("TrackStatsReporter", () => { describe("should on bytes send stats", () => { it("creating build bitrate send report.", async () => { const trackStats = new MediaTrackStats("1", "local", "video"); - TrackStatsReporter.buildBitrateSend( + TrackStatsBuilder.buildBitrateSend( trackStats, { bytesSent: 2001000, @@ -90,7 +90,7 @@ describe("TrackStatsReporter", () => { const trackStats = new MediaTrackStats("1", "remote", "video"); const remote = {} as RTCStatsReport; remote.get = jest.fn().mockReturnValue({ mimeType: "video/v8" }); - TrackStatsReporter.buildCodec(remote, trackStats, { codecId: "codecID" }); + TrackStatsBuilder.buildCodec(remote, trackStats, { codecId: "codecID" }); expect(trackStats.getCodec()).toEqual("v8"); }); }); @@ -98,7 +98,7 @@ describe("TrackStatsReporter", () => { describe("should on package lost stats", () => { it("creating build package lost on send report.", async () => { const trackStats = new MediaTrackStats("1", "local", "video"); - TrackStatsReporter.buildPacketsLost( + TrackStatsBuilder.buildPacketsLost( trackStats, { type: "outbound-rtp", @@ -114,7 +114,7 @@ describe("TrackStatsReporter", () => { }); it("creating build package lost on received report.", async () => { const trackStats = new MediaTrackStats("1", "remote", "video"); - TrackStatsReporter.buildPacketsLost( + TrackStatsBuilder.buildPacketsLost( trackStats, { type: "inbound-rtp", @@ -133,7 +133,7 @@ describe("TrackStatsReporter", () => { describe("should set state of a TrackStats", () => { it("to not alive if Transceiver undefined", async () => { const trackStats = new MediaTrackStats("1", "remote", "video"); - TrackStatsReporter.setTrackStatsState(trackStats, undefined); + TrackStatsBuilder.setTrackStatsState(trackStats, undefined); expect(trackStats.alive).toBeFalsy(); }); @@ -145,7 +145,7 @@ describe("TrackStatsReporter", () => { } as RTCRtpSender, } as RTCRtpTransceiver; - TrackStatsReporter.setTrackStatsState(trackStats, ts); + TrackStatsBuilder.setTrackStatsState(trackStats, ts); expect(trackStats.alive).toBeFalsy(); }); @@ -162,7 +162,7 @@ describe("TrackStatsReporter", () => { } as RTCRtpReceiver, } as RTCRtpTransceiver; - TrackStatsReporter.setTrackStatsState(trackStats, ts); + TrackStatsBuilder.setTrackStatsState(trackStats, ts); expect(trackStats.alive).toBeTruthy(); }); @@ -179,7 +179,7 @@ describe("TrackStatsReporter", () => { } as RTCRtpSender, } as RTCRtpTransceiver; - TrackStatsReporter.setTrackStatsState(trackStats, ts); + TrackStatsBuilder.setTrackStatsState(trackStats, ts); expect(trackStats.alive).toBeTruthy(); }); @@ -195,7 +195,7 @@ describe("TrackStatsReporter", () => { } as RTCRtpReceiver, } as RTCRtpTransceiver; - TrackStatsReporter.setTrackStatsState(trackStats, ts); + TrackStatsBuilder.setTrackStatsState(trackStats, ts); expect(trackStats.alive).toBeFalsy(); }); @@ -211,7 +211,7 @@ describe("TrackStatsReporter", () => { } as RTCRtpReceiver, } as RTCRtpTransceiver; - TrackStatsReporter.setTrackStatsState(trackStats, ts); + TrackStatsBuilder.setTrackStatsState(trackStats, ts); expect(trackStats.alive).toBeTruthy(); expect(trackStats.muted).toBeTruthy(); }); @@ -219,38 +219,46 @@ describe("TrackStatsReporter", () => { describe("should build Track Summary", () => { it("and returns empty summary if stats list empty", async () => { - const summary = TrackStatsReporter.buildTrackSummary([]); + const summary = TrackStatsBuilder.buildTrackSummary([]); expect(summary).toEqual({ audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, }); }); it("and returns summary if stats list not empty and ignore local summery", async () => { const trackStatsList = buildMockTrackStatsList(); - const summary = TrackStatsReporter.buildTrackSummary(trackStatsList); + const summary = TrackStatsBuilder.buildTrackSummary(trackStatsList); expect(summary).toEqual({ audioTrackSummary: { count: 2, muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, videoTrackSummary: { count: 3, muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, }); }); @@ -259,19 +267,23 @@ describe("TrackStatsReporter", () => { const trackStatsList = buildMockTrackStatsList(); trackStatsList[1].muted = true; trackStatsList[5].muted = true; - const summary = TrackStatsReporter.buildTrackSummary(trackStatsList); + const summary = TrackStatsBuilder.buildTrackSummary(trackStatsList); expect(summary).toEqual({ audioTrackSummary: { count: 2, muted: 1, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, videoTrackSummary: { count: 3, muted: 1, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, }); }); @@ -280,24 +292,28 @@ describe("TrackStatsReporter", () => { const trackStatsList = buildMockTrackStatsList(); trackStatsList[1].muted = true; trackStatsList[1].alive = false; - const summary = TrackStatsReporter.buildTrackSummary(trackStatsList); + const summary = TrackStatsBuilder.buildTrackSummary(trackStatsList); expect(summary).toEqual({ audioTrackSummary: { count: 2, muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, videoTrackSummary: { count: 3, muted: 0, maxJitter: 0, maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, }, }); }); - it("and returns summary and build max jitter and packet loss", async () => { + it("and returns summary and build max jitter, packet loss and audio conealment", async () => { const trackStatsList = buildMockTrackStatsList(); // video remote trackStatsList[1].setJitter(12); @@ -311,20 +327,26 @@ describe("TrackStatsReporter", () => { trackStatsList[5].setJitter(15); trackStatsList[2].setLoss({ packetsLost: 5, packetsTotal: 0, isDownloadStream: true }); trackStatsList[5].setLoss({ packetsLost: 0, packetsTotal: 0, isDownloadStream: true }); + trackStatsList[2].setAudioConcealment(220, 2000); + trackStatsList[5].setAudioConcealment(180, 2000); - const summary = TrackStatsReporter.buildTrackSummary(trackStatsList); + const summary = TrackStatsBuilder.buildTrackSummary(trackStatsList); expect(summary).toEqual({ audioTrackSummary: { count: 2, muted: 0, maxJitter: 15, maxPacketLoss: 5, + concealedAudio: 400, + totalAudio: 4000, }, videoTrackSummary: { count: 3, muted: 0, maxJitter: 66, maxPacketLoss: 55, + concealedAudio: 0, + totalAudio: 0, }, }); }); @@ -333,25 +355,25 @@ describe("TrackStatsReporter", () => { describe("should build jitter value in Track Stats", () => { it("and returns track stats without jitter if report not 'inbound-rtp'", async () => { const trackStats = new MediaTrackStats("1", "remote", "video"); - TrackStatsReporter.buildJitter(trackStats, { jitter: 0.01 }); + TrackStatsBuilder.buildJitter(trackStats, { jitter: 0.01 }); expect(trackStats.getJitter()).toEqual(0); }); it("and returns track stats with jitter", async () => { const trackStats = new MediaTrackStats("1", "remote", "video"); - TrackStatsReporter.buildJitter(trackStats, { type: "inbound-rtp", jitter: 0.01 }); + TrackStatsBuilder.buildJitter(trackStats, { type: "inbound-rtp", jitter: 0.01 }); expect(trackStats.getJitter()).toEqual(10); }); it("and returns negative jitter if stats has no jitter value", async () => { const trackStats = new MediaTrackStats("1", "remote", "video"); - TrackStatsReporter.buildJitter(trackStats, { type: "inbound-rtp" }); + TrackStatsBuilder.buildJitter(trackStats, { type: "inbound-rtp" }); expect(trackStats.getJitter()).toEqual(-1); }); it("and returns jitter as number", async () => { const trackStats = new MediaTrackStats("1", "remote", "video"); - TrackStatsReporter.buildJitter(trackStats, { type: "inbound-rtp", jitter: "0.5" }); + TrackStatsBuilder.buildJitter(trackStats, { type: "inbound-rtp", jitter: "0.5" }); expect(trackStats.getJitter()).toEqual(500); }); }); diff --git a/spec/unit/webrtc/stats/transportStatsReporter.spec.ts b/spec/unit/webrtc/stats/transportStatsBuilder.spec.ts similarity index 87% rename from spec/unit/webrtc/stats/transportStatsReporter.spec.ts rename to spec/unit/webrtc/stats/transportStatsBuilder.spec.ts index bd3288b15ae..17283f31e90 100644 --- a/spec/unit/webrtc/stats/transportStatsReporter.spec.ts +++ b/spec/unit/webrtc/stats/transportStatsBuilder.spec.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { TransportStatsReporter } from "../../../../src/webrtc/stats/transportStatsReporter"; +import { TransportStatsBuilder } from "../../../../src/webrtc/stats/transportStatsBuilder"; import { TransportStats } from "../../../../src/webrtc/stats/transportStats"; describe("TransportStatsReporter", () => { @@ -35,7 +35,7 @@ describe("TransportStatsReporter", () => { it("build new transport stats if all properties there", () => { const { report, stats } = mockStatsReport(isFocus, 0); const conferenceStatsTransport: TransportStats[] = []; - const transportStats = TransportStatsReporter.buildReport(report, stats, conferenceStatsTransport, isFocus); + const transportStats = TransportStatsBuilder.buildReport(report, stats, conferenceStatsTransport, isFocus); expect(transportStats).toEqual([ { ip: `${remoteIC.ip + 0}:${remoteIC.port}`, @@ -54,8 +54,8 @@ describe("TransportStatsReporter", () => { const mock1 = mockStatsReport(isFocus, 0); const mock2 = mockStatsReport(isFocus, 1); let transportStats: TransportStats[] = []; - transportStats = TransportStatsReporter.buildReport(mock1.report, mock1.stats, transportStats, isFocus); - transportStats = TransportStatsReporter.buildReport(mock2.report, mock2.stats, transportStats, isFocus); + transportStats = TransportStatsBuilder.buildReport(mock1.report, mock1.stats, transportStats, isFocus); + transportStats = TransportStatsBuilder.buildReport(mock2.report, mock2.stats, transportStats, isFocus); expect(transportStats).toEqual([ { ip: `${remoteIC.ip + 0}:${remoteIC.port}`, @@ -84,8 +84,8 @@ describe("TransportStatsReporter", () => { const mock1 = mockStatsReport(isFocus, 0); const mock2 = mockStatsReport(isFocus, 0); let transportStats: TransportStats[] = []; - transportStats = TransportStatsReporter.buildReport(mock1.report, mock1.stats, transportStats, isFocus); - transportStats = TransportStatsReporter.buildReport(mock2.report, mock2.stats, transportStats, isFocus); + transportStats = TransportStatsBuilder.buildReport(mock1.report, mock1.stats, transportStats, isFocus); + transportStats = TransportStatsBuilder.buildReport(mock2.report, mock2.stats, transportStats, isFocus); expect(transportStats).toEqual([ { ip: `${remoteIC.ip + 0}:${remoteIC.port}`, diff --git a/spec/unit/webrtc/stats/statsValueFormatter.spec.ts b/spec/unit/webrtc/stats/valueFormatter.spec.ts similarity index 58% rename from spec/unit/webrtc/stats/statsValueFormatter.spec.ts rename to spec/unit/webrtc/stats/valueFormatter.spec.ts index 1ce563e91d6..5009960b195 100644 --- a/spec/unit/webrtc/stats/statsValueFormatter.spec.ts +++ b/spec/unit/webrtc/stats/valueFormatter.spec.ts @@ -13,16 +13,16 @@ 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 { StatsValueFormatter } from "../../../../src/webrtc/stats/statsValueFormatter"; +import { ValueFormatter } from "../../../../src/webrtc/stats/valueFormatter"; -describe("StatsValueFormatter", () => { +describe("ValueFormatter", () => { describe("on get non negative values", () => { it("formatter shod return number", async () => { - expect(StatsValueFormatter.getNonNegativeValue("2")).toEqual(2); - expect(StatsValueFormatter.getNonNegativeValue(0)).toEqual(0); - expect(StatsValueFormatter.getNonNegativeValue("-2")).toEqual(0); - expect(StatsValueFormatter.getNonNegativeValue("")).toEqual(0); - expect(StatsValueFormatter.getNonNegativeValue(NaN)).toEqual(0); + expect(ValueFormatter.getNonNegativeValue("2")).toEqual(2); + expect(ValueFormatter.getNonNegativeValue(0)).toEqual(0); + expect(ValueFormatter.getNonNegativeValue("-2")).toEqual(0); + expect(ValueFormatter.getNonNegativeValue("")).toEqual(0); + expect(ValueFormatter.getNonNegativeValue(NaN)).toEqual(0); }); }); }); diff --git a/src/@types/event.ts b/src/@types/event.ts index 17af8df0272..a0eca5cc011 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -235,6 +235,13 @@ export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue( "org.matrix.msc3890.local_notification_settings", ); +/** + * https://github.com/matrix-org/matrix-doc/pull/4023 + * + * @experimental + */ +export const UNSIGNED_THREAD_ID_FIELD = new UnstableValue("thread_id", "org.matrix.msc4023.thread_id"); + export interface IEncryptedFile { url: string; mimetype?: string; diff --git a/src/ReEmitter.ts b/src/ReEmitter.ts index 565e8ea702c..5d41ac0e455 100644 --- a/src/ReEmitter.ts +++ b/src/ReEmitter.ts @@ -25,7 +25,7 @@ export class ReEmitter { public constructor(private readonly target: EventEmitter) {} // Map from emitter to event name to re-emitter - private reEmitters = new Map void>>(); + private reEmitters = new WeakMap void>>(); public reEmit(source: EventEmitter, eventNames: string[]): void { let reEmittersByEvent = this.reEmitters.get(source); @@ -35,6 +35,8 @@ export class ReEmitter { } for (const eventName of eventNames) { + if (reEmittersByEvent.has(eventName)) continue; + // We include the source as the last argument for event handlers which may need it, // such as read receipt listeners on the client class which won't have the context // of the room. diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index f4a34159613..5a7294b29ab 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -142,7 +142,7 @@ export class AutoDiscovery { }, }; - if (!wellknown || !wellknown["m.homeserver"]) { + if (!wellknown?.["m.homeserver"]) { logger.error("No m.homeserver key in config"); clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT; @@ -171,7 +171,7 @@ export class AutoDiscovery { // Step 3: Make sure the homeserver URL points to a homeserver. const hsVersions = await this.fetchWellKnownObject(`${hsUrl}/_matrix/client/versions`); - if (!hsVersions || !hsVersions.raw?.["versions"]) { + if (!hsVersions?.raw?.["versions"]) { logger.error("Invalid /versions response"); clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER; @@ -345,7 +345,7 @@ export class AutoDiscovery { const response = await this.fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`); if (!response) return {}; - return response.raw || {}; + return response.raw ?? {}; } /** diff --git a/src/client.ts b/src/client.ts index d02fa74fdc6..4937494b435 100644 --- a/src/client.ts +++ b/src/client.ts @@ -36,7 +36,11 @@ import { StubStore } from "./store/stub"; import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call"; import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter"; import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from "./webrtc/callEventHandler"; -import { GroupCallEventHandlerEvent, GroupCallEventHandlerEventHandlerMap } from "./webrtc/groupCallEventHandler"; +import { + GroupCallEventHandler, + GroupCallEventHandlerEvent, + GroupCallEventHandlerEventHandlerMap, +} from "./webrtc/groupCallEventHandler"; import * as utils from "./utils"; import { replaceParam, QueryDict, sleep, noUnsafeEventProps, safeSet } from "./utils"; import { Direction, EventTimeline } from "./models/event-timeline"; @@ -74,7 +78,6 @@ import { CryptoEventHandlerMap, fixBackupKey, ICryptoCallbacks, - IBootstrapCrossSigningOpts, ICheckOwnCrossSigningTrustOpts, isCryptoAvailable, VerificationMethod, @@ -181,7 +184,6 @@ import { IThreepid } from "./@types/threepids"; import { CryptoStore, OutgoingRoomKeyRequest } from "./crypto/store/base"; import { GroupCall, IGroupCallDataChannelOptions, GroupCallIntent, GroupCallType } from "./webrtc/groupCall"; import { MediaHandler } from "./webrtc/mediaHandler"; -import { GroupCallEventHandler } from "./webrtc/groupCallEventHandler"; import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth"; import { TypedEventEmitter } from "./models/typed-event-emitter"; import { MAIN_ROOM_TIMELINE, ReceiptType } from "./@types/read_receipts"; @@ -205,7 +207,7 @@ import { LocalNotificationSettings } from "./@types/local_notifications"; import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature"; import { CryptoBackend } from "./common-crypto/CryptoBackend"; import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants"; -import { CryptoApi } from "./crypto-api"; +import { BootstrapCrossSigningOpts, CryptoApi } from "./crypto-api"; import { DeviceInfoMap } from "./crypto/DeviceList"; import { AddSecretStorageKeyOpts, @@ -635,7 +637,7 @@ interface IJoinRequestBody { interface ITagMetadata { [key: string]: any; - order: number; + order?: number; } interface IMessagesResponse { @@ -2228,7 +2230,7 @@ export class MatrixClient extends TypedEventEmitterdeviceId-\>`DeviceInfo` + * + * @deprecated Prefer {@link CryptoApi.getUserDeviceInfo} */ public downloadKeys(userIds: string[], forceDownload?: boolean): Promise { if (!this.crypto) { @@ -2309,6 +2313,7 @@ export class MatrixClient extends TypedEventEmitter { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.isCrossSigningReady(); + return this.cryptoBackend.isCrossSigningReady(); } /** @@ -2747,15 +2753,15 @@ export class MatrixClient extends TypedEventEmitter { - if (!this.crypto) { + public bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.bootstrapCrossSigning(opts); + return this.cryptoBackend.bootstrapCrossSigning(opts); } /** @@ -2844,15 +2850,14 @@ export class MatrixClient extends TypedEventEmitter { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.isSecretStorageReady(); + return this.cryptoBackend.isSecretStorageReady(); } /** @@ -4085,27 +4090,23 @@ export class MatrixClient extends TypedEventEmitter(Method.Post, path, queryString, data); + const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias }); + const res = await this.http.authedRequest<{ room_id: string }>(Method.Post, path, queryString, data); - const roomId = res.room_id; - const syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions()); - const room = syncApi.createRoom(roomId); - if (opts.syncRoom) { - // v2 will do this for us - // return syncApi.syncRoom(room); - } - return room; - } catch (e) { - throw e; // rethrow for reject + const roomId = res.room_id; + const syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions()); + const syncRoom = syncApi.createRoom(roomId); + if (opts.syncRoom) { + // v2 will do this for us + // return syncApi.syncRoom(room); } + return syncRoom; } /** @@ -4185,7 +4186,7 @@ export class MatrixClient extends TypedEventEmitter { + public setRoomTag(roomId: string, tagName: string, metadata: ITagMetadata = {}): Promise<{}> { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { $userId: this.credentials.userId!, $roomId: roomId, @@ -5003,7 +5004,7 @@ export class MatrixClient extends TypedEventEmitter { const room = this.getRoom(roomId); - if (room && room.hasPendingEvent(rmEventId)) { + if (room?.hasPendingEvent(rmEventId)) { throw new Error(`Cannot set read marker to a pending event (${rmEventId})`); } @@ -5056,9 +5057,8 @@ export class MatrixClient extends TypedEventEmitter( @@ -5573,11 +5573,13 @@ export class MatrixClient extends TypedEventEmitter room.relations.aggregateChildEvent(event)); room.oldState.paginationToken = res.end ?? null; if (res.chunk.length === 0) { @@ -5686,11 +5688,12 @@ export class MatrixClient extends TypedEventEmitter timelineSet.relations.aggregateChildEvent(event)); // There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring // timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up @@ -6230,7 +6233,7 @@ export class MatrixClient extends TypedEventEmitter it.getServerAggregatedRelation(THREAD_RELATION_TYPE.name)), false, ); + unknownRelations.forEach((event) => room.relations.aggregateChildEvent(event)); const atEnd = res.end === undefined || res.end === res.start; diff --git a/src/content-repo.ts b/src/content-repo.ts index 257541296bb..d3130aa722a 100644 --- a/src/content-repo.ts +++ b/src/content-repo.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as utils from "./utils"; +import { encodeParams } from "./utils"; /** * Get the HTTP URL for an MXC URI. @@ -74,6 +74,6 @@ export function getHttpUriForMxc( serverAndMediaId = serverAndMediaId.slice(0, fragmentOffset); } - const urlParams = Object.keys(params).length === 0 ? "" : "?" + utils.encodeParams(params); + const urlParams = Object.keys(params).length === 0 ? "" : "?" + encodeParams(params); return baseUrl + prefix + serverAndMediaId + urlParams + fragment; } diff --git a/src/crypto-api.ts b/src/crypto-api.ts index bf3213f22f8..1149b036d04 100644 --- a/src/crypto-api.ts +++ b/src/crypto-api.ts @@ -17,6 +17,14 @@ limitations under the License. import type { IMegolmSessionData } from "./@types/crypto"; import { Room } from "./models/room"; import { DeviceMap } from "./models/device"; +import { UIAuthCallback } from "./interactive-auth"; + +/** Types of cross-signing key */ +export enum CrossSigningKey { + Master = "master", + SelfSigning = "self_signing", + UserSigning = "user_signing", +} /** * Public interface to the cryptography parts of the js-sdk @@ -106,7 +114,7 @@ export interface CryptoApi { /** * Return whether we trust other user's signatures of their devices. * - * @see {@link CryptoApi#setTrustCrossSignedDevices} + * @see {@link Crypto.CryptoApi#setTrustCrossSignedDevices} * * @returns `true` if we trust cross-signed devices, otherwise `false`. */ @@ -118,9 +126,79 @@ export interface CryptoApi { * @param userId - The ID of the user whose device is to be checked. * @param deviceId - The ID of the device to check * - * @returns Verification status of the device, or `null` if the device is not known + * @returns `null` if the device is unknown, or has not published any encryption keys (implying it does not support + * encryption); otherwise the verification status of the device. */ getDeviceVerificationStatus(userId: string, deviceId: string): Promise; + + /** + * Checks whether cross signing: + * - is enabled on this account and trusted by this device + * - has private keys either cached locally or stored in secret storage + * + * If this function returns false, bootstrapCrossSigning() can be used + * to fix things such that it returns true. That is to say, after + * bootstrapCrossSigning() completes successfully, this function should + * return true. + * + * @returns True if cross-signing is ready to be used on this device + */ + isCrossSigningReady(): Promise; + + /** + * Get the ID of one of the user's cross-signing keys. + * + * @param type - The type of key to get the ID of. One of `CrossSigningKey.Master`, `CrossSigningKey.SelfSigning`, + * or `CrossSigningKey.UserSigning`. Defaults to `CrossSigningKey.Master`. + * + * @returns If cross-signing has been initialised on this device, the ID of the given key. Otherwise, null + */ + getCrossSigningKeyId(type?: CrossSigningKey): Promise; + + /** + * Bootstrap cross-signing by creating keys if needed. + * + * If everything is already set up, then no changes are made, so this is safe to run to ensure + * cross-signing is ready for use. + * + * This function: + * - creates new cross-signing keys if they are not found locally cached nor in + * secret storage (if it has been set up) + * - publishes the public keys to the server if they are not already published + * - stores the private keys in secret storage if secret storage is set up. + * + * @param opts - options object + */ + bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise; + + /** + * Checks whether secret storage: + * - is enabled on this account + * - is storing cross-signing private keys + * - is storing session backup key (if enabled) + * + * If this function returns false, bootstrapSecretStorage() can be used + * to fix things such that it returns true. That is to say, after + * bootstrapSecretStorage() completes successfully, this function should + * return true. + * + * @returns True if secret storage is ready to be used on this device + */ + isSecretStorageReady(): Promise; +} + +/** + * Options object for `CryptoApi.bootstrapCrossSigning`. + */ +export interface BootstrapCrossSigningOpts { + /** Optional. Reset the cross-signing keys even if keys already exist. */ + setupNewCrossSigning?: boolean; + + /** + * An application callback to collect the authentication data for uploading the keys. If not given, the keys + * will not be uploaded to the server (which seems like a bad thing?). + */ + authUploadDeviceSigningKeys?: UIAuthCallback; } export class DeviceVerificationStatus { @@ -175,7 +253,7 @@ export class DeviceVerificationStatus { * A device is "verified" if either: * * it has been manually marked as such via {@link MatrixClient#setDeviceVerified}. * * it has been cross-signed with a verified signing key, **and** the client has been configured to trust - * cross-signed devices via {@link CryptoApi#setTrustCrossSignedDevices}. + * cross-signed devices via {@link Crypto.CryptoApi#setTrustCrossSignedDevices}. * * @returns true if this device is verified via any means. */ @@ -183,3 +261,5 @@ export class DeviceVerificationStatus { return this.localVerified || (this.trustCrossSignedDevices && this.crossSigningVerified); } } + +export * from "./crypto-api/verification"; diff --git a/src/crypto-api/verification.ts b/src/crypto-api/verification.ts new file mode 100644 index 00000000000..b78c713eeb2 --- /dev/null +++ b/src/crypto-api/verification.ts @@ -0,0 +1,112 @@ +/* +Copyright 2023 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 { MatrixEvent } from "../models/event"; + +/** Events emitted by `Verifier`. */ +export enum VerifierEvent { + /** + * The verification has been cancelled, by us or the other side. + * + * The payload is either an {@link Error}, or an (incoming or outgoing) {@link MatrixEvent}, depending on + * unspecified reasons. + */ + Cancel = "cancel", + + /** + * SAS data has been exchanged and should be displayed to the user. + * + * The payload is the {@link ShowQrCodeCallbacks} object. + */ + ShowSas = "show_sas", + + /** + * QR code data should be displayed to the user. + * + * The payload is the {@link ShowQrCodeCallbacks} object. + */ + ShowReciprocateQr = "show_reciprocate_qr", +} + +/** Listener type map for {@link VerifierEvent}s. */ +export type VerifierEventHandlerMap = { + [VerifierEvent.Cancel]: (e: Error | MatrixEvent) => void; + [VerifierEvent.ShowSas]: (sas: ShowSasCallbacks) => void; + [VerifierEvent.ShowReciprocateQr]: (qr: ShowQrCodeCallbacks) => void; +}; + +/** + * Callbacks for user actions while a QR code is displayed. + * + * This is exposed as the payload of a `VerifierEvent.ShowReciprocateQr` event, or can be retrieved directly from the + * verifier as `reciprocateQREvent`. + */ +export interface ShowQrCodeCallbacks { + /** The user confirms that the verification data matches */ + confirm(): void; + + /** Cancel the verification flow */ + cancel(): void; +} + +/** + * Callbacks for user actions while a SAS is displayed. + * + * This is exposed as the payload of a `VerifierEvent.ShowSas` event, or directly from the verifier as `sasEvent`. + */ +export interface ShowSasCallbacks { + /** The generated SAS to be shown to the user */ + sas: GeneratedSas; + + /** Function to call if the user confirms that the SAS matches. + * + * @returns A Promise that completes once the m.key.verification.mac is queued. + */ + confirm(): Promise; + + /** + * Function to call if the user finds the SAS does not match. + * + * Sends an `m.key.verification.cancel` event with a `m.mismatched_sas` error code. + */ + mismatch(): void; + + /** Cancel the verification flow */ + cancel(): void; +} + +/** A generated SAS to be shown to the user, in alternative formats */ +export interface GeneratedSas { + /** + * The SAS as three numbers between 0 and 8191. + * + * Only populated if the `decimal` SAS method was negotiated. + */ + decimal?: [number, number, number]; + + /** + * The SAS as seven emojis. + * + * Only populated if the `emoji` SAS method was negotiated. + */ + emoji?: EmojiMapping[]; +} + +/** + * An emoji for the generated SAS. A tuple `[emoji, name]` where `emoji` is the emoji itself and `name` is the + * English name. + */ +export type EmojiMapping = [emoji: string, name: string]; diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index a77682e4cf0..e71967cb2a5 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -688,7 +688,7 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O _expectedPublicKey: string, ): Promise { const key = await new Promise((resolve) => { - return store.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { + store.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { store.getSecretStorePrivateKey(txn, resolve, type); }); }); @@ -790,7 +790,7 @@ export async function requestKeysDuringVerification( })(); // We call getCrossSigningKey() for its side-effects - return Promise.race([ + Promise.race([ Promise.all([ crossSigning.getCrossSigningKey("master"), crossSigning.getCrossSigningKey("self_signing"), diff --git a/src/crypto/DeviceList.ts b/src/crypto/DeviceList.ts index a1ff0ebf144..8ad3831893a 100644 --- a/src/crypto/DeviceList.ts +++ b/src/crypto/DeviceList.ts @@ -116,7 +116,7 @@ export class DeviceList extends TypedEventEmitter { await this.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { this.cryptoStore.getEndToEndDeviceData(txn, (deviceData) => { - this.hasFetched = Boolean(deviceData && deviceData.devices); + this.hasFetched = Boolean(deviceData?.devices); this.devices = deviceData ? deviceData.devices : {}; this.crossSigningInfo = deviceData ? deviceData.crossSigningInfo || {} : {}; this.deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {}; diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index b09df261930..b8f52fcdcea 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -19,7 +19,7 @@ import { MatrixEvent } from "../models/event"; import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning"; import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store"; import { Method, ClientPrefix } from "../http-api"; -import { Crypto, ICryptoCallbacks, IBootstrapCrossSigningOpts } from "./index"; +import { Crypto, ICryptoCallbacks } from "./index"; import { ClientEvent, ClientEventHandlerMap, @@ -31,9 +31,10 @@ import { import { IKeyBackupInfo } from "./keybackup"; import { TypedEventEmitter } from "../models/typed-event-emitter"; import { AccountDataClient, SecretStorageKeyDescription } from "../secret-storage"; +import { BootstrapCrossSigningOpts } from "../crypto-api"; interface ICrossSigningKeys { - authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"]; + authUpload: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"]; keys: Record<"master" | "self_signing" | "user_signing", ICrossSigningKey>; } diff --git a/src/crypto/api.ts b/src/crypto/api.ts index 571c3df78d3..97ead425dab 100644 --- a/src/crypto/api.ts +++ b/src/crypto/api.ts @@ -19,6 +19,7 @@ import { IKeyBackupInfo } from "./keybackup"; import type { AddSecretStorageKeyOpts } from "../secret-storage"; /* re-exports for backwards compatibility. */ +export { CrossSigningKey } from "../crypto-api"; export type { AddSecretStorageKeyOpts as IAddSecretStorageKeyOpts, PassphraseInfo as IPassphraseInfo, @@ -27,12 +28,6 @@ export type { // TODO: Merge this with crypto.js once converted -export enum CrossSigningKey { - Master = "master", - SelfSigning = "self_signing", - UserSigning = "user_signing", -} - export interface IEncryptedEventInfo { /** * whether the event is encrypted (if not encrypted, some of the other properties may not be set) diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 9554e617359..19989d3b59f 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -35,7 +35,13 @@ import * as algorithms from "./algorithms"; import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./CrossSigning"; import { EncryptionSetupBuilder } from "./EncryptionSetup"; import { SecretStorage as LegacySecretStorage } from "./SecretStorage"; -import { ICreateSecretStorageOpts, IEncryptedEventInfo, IImportRoomKeysOpts, IRecoveryKey } from "./api"; +import { + CrossSigningKey, + ICreateSecretStorageOpts, + IEncryptedEventInfo, + IImportRoomKeysOpts, + IRecoveryKey, +} from "./api"; import { OutgoingRoomKeyRequestManager } from "./OutgoingRoomKeyRequestManager"; import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store"; import { VerificationBase } from "./verification/Base"; @@ -45,7 +51,7 @@ import { keyFromPassphrase } from "./key_passphrase"; import { decodeRecoveryKey, encodeRecoveryKey } from "./recoverykey"; import { VerificationRequest } from "./verification/request/VerificationRequest"; import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChannel"; -import { ToDeviceChannel, ToDeviceRequests, Request } from "./verification/request/ToDeviceChannel"; +import { Request, ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel"; import { IllegalMethod } from "./verification/IllegalMethod"; import { KeySignatureUploadError } from "../errors"; import { calculateKeyCheck, decryptAES, encryptAES } from "./aes"; @@ -54,7 +60,7 @@ import { BackupManager } from "./backup"; import { IStore } from "../store"; import { Room, RoomEvent } from "../models/room"; import { RoomMember, RoomMemberEvent } from "../models/room-member"; -import { EventStatus, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event"; +import { EventStatus, IContent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event"; import { ToDeviceBatch } from "../models/ToDeviceMessage"; import { ClientEvent, @@ -70,7 +76,6 @@ 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 { IDeviceLists, ISyncResponse, IToDeviceEvent } from "../sync-accumulator"; import { ISignatures } from "../@types/signed"; import { IMessage } from "./algorithms/olm"; @@ -80,18 +85,21 @@ import { MapWithDefault, recursiveMapToObject } from "../utils"; import { AccountDataClient, AddSecretStorageKeyOpts, + SECRET_STORAGE_ALGORITHM_V1_AES, + SecretStorageCallbacks, SecretStorageKeyDescription, SecretStorageKeyObject, SecretStorageKeyTuple, - SECRET_STORAGE_ALGORITHM_V1_AES, - SecretStorageCallbacks, ServerSideSecretStorageImpl, } from "../secret-storage"; import { ISecretRequest } from "./SecretSharing"; -import { DeviceVerificationStatus } from "../crypto-api"; +import { BootstrapCrossSigningOpts, DeviceVerificationStatus } from "../crypto-api"; import { Device, DeviceMap } from "../models/device"; import { deviceInfoToDevice } from "./device-converter"; +/* re-exports for backwards compatibility */ +export type { BootstrapCrossSigningOpts as IBootstrapCrossSigningOpts } from "../crypto-api"; + const DeviceVerification = DeviceInfo.DeviceVerification; const defaultVerificationMethods = { @@ -127,16 +135,6 @@ interface IInitOpts { pickleKey?: string; } -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; -} - export interface ICryptoCallbacks extends SecretStorageCallbacks { getCrossSigningKey?: (keyType: string, pubKey: string) => Promise; saveCrossSigningKeys?: (keys: Record) => void; @@ -613,7 +611,7 @@ export class Crypto extends TypedEventEmitter { + }: BootstrapCrossSigningOpts = {}): Promise { logger.log("Bootstrapping cross-signing"); const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks; @@ -1422,6 +1420,11 @@ export class Crypto extends TypedEventEmitter { + return Promise.resolve(this.getCrossSigningId(type)); + } + + // old name, for backwards compatibility public getCrossSigningId(type: string): string | null { return this.crossSigningInfo.getId(type); } @@ -1470,7 +1473,7 @@ export class Crypto extends TypedEventEmitter { const requestBody = request.requestBody; - return utils.promiseTry(() => { + return promiseTry(() => { // first see if we already have an entry for this request. const existing = this._getOutgoingRoomKeyRequest(requestBody); @@ -138,7 +137,7 @@ export class MemoryCryptoStore implements CryptoStore { // eslint-disable-next-line @typescript-eslint/naming-convention private _getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): OutgoingRoomKeyRequest | null { for (const existing of this.outgoingRoomKeyRequests) { - if (utils.deepCompare(existing.requestBody, requestBody)) { + if (deepCompare(existing.requestBody, requestBody)) { return existing; } } diff --git a/src/crypto/verification/Base.ts b/src/crypto/verification/Base.ts index 89c700c231c..89bead3aa3e 100644 --- a/src/crypto/verification/Base.ts +++ b/src/crypto/verification/Base.ts @@ -28,7 +28,8 @@ import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossS import { IVerificationChannel } from "./request/Channel"; import { MatrixClient } from "../../client"; import { VerificationRequest } from "./request/VerificationRequest"; -import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter"; +import { TypedEventEmitter } from "../../models/typed-event-emitter"; +import { VerifierEvent, VerifierEventHandlerMap } from "../../crypto-api/verification"; const timeoutException = new Error("Verification timed out"); @@ -40,18 +41,24 @@ export class SwitchStartEventError extends Error { export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void; -export enum VerificationEvent { - Cancel = "cancel", -} +/** @deprecated use VerifierEvent */ +export type VerificationEvent = VerifierEvent; +/** @deprecated use VerifierEvent */ +export const VerificationEvent = VerifierEvent; +/** @deprecated use VerifierEventHandlerMap */ export type VerificationEventHandlerMap = { [VerificationEvent.Cancel]: (e: Error | MatrixEvent) => void; }; +// The type parameters of VerificationBase are no longer used, but we need some placeholders to maintain +// backwards compatibility with applications that reference the class. export class VerificationBase< - Events extends string, - Arguments extends ListenerMap, -> extends TypedEventEmitter { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + Events extends string = VerifierEvent, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + Arguments = VerifierEventHandlerMap, +> extends TypedEventEmitter { private cancelled = false; private _done = false; private promise: Promise | null = null; diff --git a/src/crypto/verification/QRCode.ts b/src/crypto/verification/QRCode.ts index bfb532e4223..38feade12db 100644 --- a/src/crypto/verification/QRCode.ts +++ b/src/crypto/verification/QRCode.ts @@ -18,7 +18,7 @@ limitations under the License. * QR code key verification. */ -import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base"; +import { VerificationBase as Base } from "./Base"; import { newKeyMismatchError, newUserCancelledError } from "./Error"; import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib"; import { logger } from "../../logger"; @@ -26,25 +26,18 @@ import { VerificationRequest } from "./request/VerificationRequest"; import { MatrixClient } from "../../client"; import { IVerificationChannel } from "./request/Channel"; import { MatrixEvent } from "../../models/event"; +import { ShowQrCodeCallbacks, VerifierEvent } from "../../crypto-api/verification"; export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1"; export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1"; -interface IReciprocateQr { - confirm(): void; - cancel(): void; -} - -export enum QrCodeEvent { - ShowReciprocateQr = "show_reciprocate_qr", -} - -type EventHandlerMap = { - [QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void; -} & VerificationEventHandlerMap; +/** @deprecated use VerifierEvent */ +export type QrCodeEvent = VerifierEvent; +/** @deprecated use VerifierEvent */ +export const QrCodeEvent = VerifierEvent; -export class ReciprocateQRCode extends Base { - public reciprocateQREvent?: IReciprocateQr; +export class ReciprocateQRCode extends Base { + public reciprocateQREvent?: ShowQrCodeCallbacks; public static factory( channel: IVerificationChannel, diff --git a/src/crypto/verification/SAS.ts b/src/crypto/verification/SAS.ts index a8d237d2da1..61f096ca452 100644 --- a/src/crypto/verification/SAS.ts +++ b/src/crypto/verification/SAS.ts @@ -21,7 +21,7 @@ limitations under the License. import anotherjson from "another-json"; import { Utility, SAS as OlmSAS } from "@matrix-org/olm"; -import { VerificationBase as Base, SwitchStartEventError, VerificationEventHandlerMap } from "./Base"; +import { VerificationBase as Base, SwitchStartEventError } from "./Base"; import { errorFactory, newInvalidMessageError, @@ -33,6 +33,14 @@ import { logger } from "../../logger"; import { IContent, MatrixEvent } from "../../models/event"; import { generateDecimalSas } from "./SASDecimal"; import { EventType } from "../../@types/event"; +import { EmojiMapping, GeneratedSas, ShowSasCallbacks, VerifierEvent } from "../../crypto-api/verification"; + +// backwards-compatibility exports +export type { + ShowSasCallbacks as ISasEvent, + GeneratedSas as IGeneratedSas, + EmojiMapping, +} from "../../crypto-api/verification"; const START_TYPE = EventType.KeyVerificationStart; @@ -44,8 +52,6 @@ const newMismatchedSASError = errorFactory("m.mismatched_sas", "Mismatched short const newMismatchedCommitmentError = errorFactory("m.mismatched_commitment", "Mismatched commitment"); -type EmojiMapping = [emoji: string, name: string]; - const emojiMapping: EmojiMapping[] = [ ["🐶", "dog"], // 0 ["🐱", "cat"], // 1 @@ -133,20 +139,8 @@ const sasGenerators = { emoji: generateEmojiSas, } as const; -export interface IGeneratedSas { - decimal?: [number, number, number]; - emoji?: EmojiMapping[]; -} - -export interface ISasEvent { - sas: IGeneratedSas; - confirm(): Promise; - cancel(): void; - mismatch(): void; -} - -function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas { - const sas: IGeneratedSas = {}; +function generateSas(sasBytes: Uint8Array, methods: string[]): GeneratedSas { + const sas: GeneratedSas = {}; for (const method of methods) { if (method in sasGenerators) { // @ts-ignore - ts doesn't like us mixing types like this @@ -220,19 +214,16 @@ function intersection(anArray: T[], aSet: Set): T[] { return Array.isArray(anArray) ? anArray.filter((x) => aSet.has(x)) : []; } -export enum SasEvent { - ShowSas = "show_sas", -} - -type EventHandlerMap = { - [SasEvent.ShowSas]: (sas: ISasEvent) => void; -} & VerificationEventHandlerMap; +/** @deprecated use VerifierEvent */ +export type SasEvent = VerifierEvent; +/** @deprecated use VerifierEvent */ +export const SasEvent = VerifierEvent; -export class SAS extends Base { +export class SAS extends Base { private waitingForAccept?: boolean; public ourSASPubKey?: string; public theirSASPubKey?: string; - public sasEvent?: ISasEvent; + public sasEvent?: ShowSasCallbacks; // eslint-disable-next-line @typescript-eslint/naming-convention public static get NAME(): string { diff --git a/src/crypto/verification/request/InRoomChannel.ts b/src/crypto/verification/request/InRoomChannel.ts index ff11bf192bb..d66032bf8f1 100644 --- a/src/crypto/verification/request/InRoomChannel.ts +++ b/src/crypto/verification/request/InRoomChannel.ts @@ -125,7 +125,7 @@ export class InRoomChannel implements IVerificationChannel { // part of a verification request, so be noisy when rejecting something if (type === REQUEST_TYPE) { if (!content || typeof content.to !== "string" || !content.to.length) { - logger.log("InRoomChannel: validateEvent: " + "no valid to " + (content && content.to)); + logger.log("InRoomChannel: validateEvent: " + "no valid to " + content.to); return false; } @@ -134,7 +134,7 @@ export class InRoomChannel implements IVerificationChannel { logger.log( "InRoomChannel: validateEvent: " + `not directed to or sent by me: ${event.getSender()}` + - `, ${content && content.to}`, + `, ${content.to}`, ); return false; } @@ -208,10 +208,17 @@ export class InRoomChannel implements IVerificationChannel { this.requestEventId = InRoomChannel.getTransactionId(event); } + // With pendingEventOrdering: "chronological", we will see events that have been sent but not yet reflected + // back via /sync. These are "local echoes" and are identifiable by their txnId + const isLocalEcho = !!event.getTxnId(); + + // Alternatively, we may see an event that we sent that is reflected back via /sync. These are "remote echoes" + // and have a transaction ID in the "unsigned" data const isRemoteEcho = !!event.getUnsigned().transaction_id; + const isSentByUs = event.getSender() === this.client.getUserId(); - return request.handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs); + return request.handleEvent(type, event, isLiveEvent, isLocalEcho || isRemoteEcho, isSentByUs); } /** diff --git a/src/embedded.ts b/src/embedded.ts index a08b79a2f5c..c2c30105a42 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -25,14 +25,13 @@ import { ISendEventFromWidgetResponseData, } from "matrix-widget-api"; -import { IEvent, IContent, EventStatus } from "./models/event"; +import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event"; import { ISendEventResponse } from "./@types/requests"; import { EventType } from "./@types/event"; import { logger } from "./logger"; import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts, SendToDeviceContentMap } from "./client"; import { SyncApi, SyncState } from "./sync"; import { SlidingSyncSdk } from "./sliding-sync-sdk"; -import { MatrixEvent } from "./models/event"; import { User } from "./models/user"; import { Room } from "./models/room"; import { ToDeviceBatch, ToDevicePayload } from "./models/ToDeviceMessage"; diff --git a/src/http-api/fetch.ts b/src/http-api/fetch.ts index e1c266c64d1..5b0f0a1bacc 100644 --- a/src/http-api/fetch.ts +++ b/src/http-api/fetch.ts @@ -18,7 +18,7 @@ limitations under the License. * This is an internal module. See {@link MatrixHttpApi} for the public class. */ -import * as utils from "../utils"; +import { checkObjectHasKeys, encodeParams } from "../utils"; import { TypedEventEmitter } from "../models/typed-event-emitter"; import { Method } from "./method"; import { ConnectionError, MatrixError } from "./errors"; @@ -45,7 +45,7 @@ export class FetchHttpApi { private eventEmitter: TypedEventEmitter, public readonly opts: O, ) { - utils.checkObjectHasKeys(opts, ["baseUrl", "prefix"]); + checkObjectHasKeys(opts, ["baseUrl", "prefix"]); opts.onlyData = !!opts.onlyData; opts.useAuthorizationHeader = opts.useAuthorizationHeader ?? true; } @@ -304,7 +304,7 @@ export class FetchHttpApi { public getUrl(path: string, queryParams?: QueryDict, prefix?: string, baseUrl?: string): URL { const url = new URL((baseUrl ?? this.opts.baseUrl) + (prefix ?? this.opts.prefix) + path); if (queryParams) { - utils.encodeParams(queryParams, url.searchParams); + encodeParams(queryParams, url.searchParams); } return url; } diff --git a/src/http-api/index.ts b/src/http-api/index.ts index c5e8e2a3a9a..1de1f847b27 100644 --- a/src/http-api/index.ts +++ b/src/http-api/index.ts @@ -17,7 +17,7 @@ limitations under the License. import { FetchHttpApi } from "./fetch"; import { FileType, IContentUri, IHttpOpts, Upload, UploadOpts, UploadResponse } from "./interface"; import { MediaPrefix } from "./prefix"; -import * as utils from "../utils"; +import { defer, QueryDict, removeElement } from "../utils"; import * as callbacks from "../realtime-callbacks"; import { Method } from "./method"; import { ConnectionError } from "./errors"; @@ -58,14 +58,14 @@ export class MatrixHttpApi extends FetchHttpApi { total: 0, abortController, } as Upload; - const defer = utils.defer(); + const deferred = defer(); if (global.XMLHttpRequest) { const xhr = new global.XMLHttpRequest(); const timeoutFn = function (): void { xhr.abort(); - defer.reject(new Error("Timeout")); + deferred.reject(new Error("Timeout")); }; // set an initial timeout of 30s; we'll advance it each time we get a progress notification @@ -84,16 +84,16 @@ export class MatrixHttpApi extends FetchHttpApi { } if (xhr.status >= 400) { - defer.reject(parseErrorResponse(xhr, xhr.responseText)); + deferred.reject(parseErrorResponse(xhr, xhr.responseText)); } else { - defer.resolve(JSON.parse(xhr.responseText)); + deferred.resolve(JSON.parse(xhr.responseText)); } } catch (err) { if ((err).name === "AbortError") { - defer.reject(err); + deferred.reject(err); return; } - defer.reject(new ConnectionError("request failed", err)); + deferred.reject(new ConnectionError("request failed", err)); } break; } @@ -131,7 +131,7 @@ export class MatrixHttpApi extends FetchHttpApi { xhr.abort(); }); } else { - const queryParams: utils.QueryDict = {}; + const queryParams: QueryDict = {}; if (includeFilename && fileName) { queryParams.filename = fileName; } @@ -146,16 +146,16 @@ export class MatrixHttpApi extends FetchHttpApi { .then((response) => { return this.opts.onlyData ? response : response.json(); }) - .then(defer.resolve, defer.reject); + .then(deferred.resolve, deferred.reject); } // remove the upload from the list on completion - upload.promise = defer.promise.finally(() => { - utils.removeElement(this.uploads, (elem) => elem === upload); + upload.promise = deferred.promise.finally(() => { + removeElement(this.uploads, (elem) => elem === upload); }); abortController.signal.addEventListener("abort", () => { - utils.removeElement(this.uploads, (elem) => elem === upload); - defer.reject(new DOMException("Aborted", "AbortError")); + removeElement(this.uploads, (elem) => elem === upload); + deferred.reject(new DOMException("Aborted", "AbortError")); }); this.uploads.push(upload); return upload.promise; diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts index 7c78bd0f434..4d3f643f0d7 100644 --- a/src/interactive-auth.ts +++ b/src/interactive-auth.ts @@ -20,12 +20,13 @@ import { logger } from "./logger"; import { MatrixClient } from "./client"; import { defer, IDeferred } from "./utils"; import { MatrixError } from "./http-api"; +import { UIAResponse } from "./@types/uia"; const EMAIL_STAGE_TYPE = "m.login.email.identity"; const MSISDN_STAGE_TYPE = "m.login.msisdn"; export interface UIAFlow { - stages: AuthType[]; + stages: Array; } export interface IInputs { @@ -118,6 +119,16 @@ export class NoAuthFlowFoundError extends Error { } } +/** + * The type of an application callback to perform the user-interactive bit of UIA. + * + * It is called with a single parameter, `makeRequest`, which is a function which takes the UIA parameters and + * makes the HTTP request. + * + * The generic parameter `T` is the type of the response of the endpoint, once it is eventually successful. + */ +export type UIAuthCallback = (makeRequest: (authData: IAuthDict) => Promise>) => Promise; + interface IOpts { /** * A matrix client to use for the auth process @@ -145,6 +156,14 @@ interface IOpts { */ emailSid?: string; + /** + * If specified, will prefer flows which entirely consist of listed stages. + * These should normally be of type AuthTypes but can be string when supporting custom auth stages. + * + * This can be used to avoid needing the fallback mechanism. + */ + supportedStages?: Array; + /** * 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. @@ -165,7 +184,7 @@ interface IOpts { * m.login.email.identity: * * emailSid: string, the sid of the active email auth session */ - stateUpdated(nextStage: AuthType, status: IStageStatus): void; + stateUpdated(nextStage: AuthType | string, status: IStageStatus): void; /** * A function that takes the email address (string), clientSecret (string), attempt number (int) and @@ -205,6 +224,7 @@ export class InteractiveAuth { private readonly busyChangedCallback?: IOpts["busyChanged"]; private readonly stateUpdatedCallback: IOpts["stateUpdated"]; private readonly requestEmailTokenCallback: IOpts["requestEmailToken"]; + private readonly supportedStages?: Set; private data: IAuthData; private emailSid?: string; @@ -232,6 +252,7 @@ export class InteractiveAuth { if (opts.sessionId) this.data.session = opts.sessionId; this.clientSecret = opts.clientSecret || this.matrixClient.generateClientSecret(); this.emailSid = opts.emailSid; + if (opts.supportedStages !== undefined) this.supportedStages = new Set(opts.supportedStages); } /** @@ -560,7 +581,7 @@ export class InteractiveAuth { * @returns login type * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found */ - private chooseStage(): AuthType | undefined { + private chooseStage(): AuthType | string | undefined { if (this.chosenFlow === null) { this.chosenFlow = this.chooseFlow(); } @@ -570,6 +591,17 @@ export class InteractiveAuth { return nextStage; } + // Returns a low number for flows we consider best. Counts increase for longer flows and even more so + // for flows which contain stages not listed in `supportedStages`. + private scoreFlow(flow: UIAFlow): number { + let score = flow.stages.length; + if (this.supportedStages !== undefined) { + // Add 10 points to the score for each unsupported stage in the flow. + score += flow.stages.filter((stage) => !this.supportedStages!.has(stage)).length * 10; + } + return score; + } + /** * Pick one of the flows from the returned list * If a flow using all of the inputs is found, it will @@ -592,6 +624,10 @@ export class InteractiveAuth { const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid); const haveMsisdn = Boolean(this.inputs.phoneCountry) && Boolean(this.inputs.phoneNumber); + // Flows are not represented in a significant order, so we can choose any we support best + // Sort flows based on how many unsupported stages they contain ascending + flows.sort((a, b) => this.scoreFlow(a) - this.scoreFlow(b)); + for (const flow of flows) { let flowHasEmail = false; let flowHasMsisdn = false; @@ -622,7 +658,7 @@ export class InteractiveAuth { * @internal * @returns login type */ - private firstUncompletedStage(flow: UIAFlow): AuthType | undefined { + private firstUncompletedStage(flow: UIAFlow): AuthType | string | undefined { const completed = this.data.completed || []; return flow.stages.find((stageType) => !completed.includes(stageType)); } diff --git a/src/matrix.ts b/src/matrix.ts index a60376df6a4..e7fd2ad75f0 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -37,6 +37,7 @@ export * from "./models/event-timeline-set"; export * from "./models/poll"; export * from "./models/room-member"; export * from "./models/room-state"; +export * from "./models/typed-event-emitter"; export * from "./models/user"; export * from "./models/device"; export * from "./scheduler"; @@ -63,9 +64,29 @@ export { createNewMatrixCall } from "./webrtc/call"; export type { MatrixCall } from "./webrtc/call"; export { GroupCallEvent, GroupCallIntent, GroupCallState, GroupCallType } from "./webrtc/groupCall"; export type { GroupCall } from "./webrtc/groupCall"; +export { CryptoEvent } from "./crypto"; + +/** + * Types supporting cryptography. + * + * The most important is {@link Crypto.CryptoApi}, an instance of which can be retrieved via + * {@link MatrixClient.getCrypto}. + */ +export * as Crypto from "./crypto-api"; + +/** + * Backwards compatibility re-export + * @internal + * @deprecated use {@link Crypto.CryptoApi} + */ export type { CryptoApi } from "./crypto-api"; + +/** + * Backwards compatibility re-export + * @internal + * @deprecated use {@link Crypto.DeviceVerificationStatus} + */ export { DeviceVerificationStatus } from "./crypto-api"; -export { CryptoEvent } from "./crypto"; let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore(); diff --git a/src/models/device.ts b/src/models/device.ts index 6f63e2e0634..0a451fd5a8d 100644 --- a/src/models/device.ts +++ b/src/models/device.ts @@ -27,7 +27,7 @@ export type DeviceMap = Map>; type DeviceParameters = Pick & Partial; /** - * Information on a user's device, as returned by {@link CryptoApi.getUserDeviceInfo}. + * Information on a user's device, as returned by {@link Crypto.CryptoApi.getUserDeviceInfo}. */ export class Device { /** id of the device */ diff --git a/src/models/event-timeline-set.ts b/src/models/event-timeline-set.ts index 5cb04997e8b..dfe9694c2ed 100644 --- a/src/models/event-timeline-set.ts +++ b/src/models/event-timeline-set.ts @@ -756,6 +756,94 @@ export class EventTimelineSet extends TypedEventEmitter event.getTs()) { + // We found an event later than ours, so insert before that. + break; + } + } + // If we got to the end of the loop, insertIndex points at the end of + // the list. + + const eventId = event.getId()!; + timeline.insertEvent(event, insertIndex, roomState); + this._eventIdToTimeline.set(eventId, timeline); + + this.relations.aggregateParentEvent(event); + this.relations.aggregateChildEvent(event, this); + + const data: IRoomTimelineData = { + timeline: timeline, + liveEvent: timeline == this.liveTimeline, + }; + this.emit(RoomEvent.Timeline, event, this.room, false, false, data); + } + /** * Replaces event with ID oldEventId with one with newEventId, if oldEventId is * recognised. Otherwise, add to the live timeline. Used to handle remote echos. diff --git a/src/models/event-timeline.ts b/src/models/event-timeline.ts index d1ba3210365..a20dac880e4 100644 --- a/src/models/event-timeline.ts +++ b/src/models/event-timeline.ts @@ -427,6 +427,45 @@ export class EventTimeline { } } + /** + * Insert a new event into the timeline, and update the state. + * + * TEMPORARY: until we have recursive relations, we need this function + * to exist to allow us to insert events in timeline order, which is our + * best guess for Sync Order. + * This is a copy of addEvent above, modified to allow inserting an event at + * a specific index. + * + * @internal + */ + public insertEvent(event: MatrixEvent, insertIndex: number, roomState: RoomState): void { + const timelineSet = this.getTimelineSet(); + + if (timelineSet.room) { + EventTimeline.setEventMetadata(event, roomState, false); + + // modify state but only on unfiltered timelineSets + if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) { + roomState.setStateEvents([event], {}); + // it is possible that the act of setting the state event means we + // can set more metadata (specifically sender/target props), so try + // it again if the prop wasn't previously set. It may also mean that + // the sender/target is updated (if the event set was a room member event) + // so we want to use the *updated* member (new avatar/name) instead. + // + // However, we do NOT want to do this on member events if we're going + // back in time, else we'll set the .sender value for BEFORE the given + // member event, whereas we want to set the .sender value for the ACTUAL + // member event itself. + if (!event.sender || event.getType() === EventType.RoomMember) { + EventTimeline.setEventMetadata(event, roomState, false); + } + } + } + + this.events.splice(insertIndex, 0, event); // insert element + } + /** * Remove an event from the timeline * diff --git a/src/models/event.ts b/src/models/event.ts index 5c55449a3f5..feb21fbba74 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -24,7 +24,13 @@ import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk"; import type { IEventDecryptionResult } from "../@types/crypto"; import { logger } from "../logger"; import { VerificationRequest } from "../crypto/verification/request/VerificationRequest"; -import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event"; +import { + EVENT_VISIBILITY_CHANGE_TYPE, + EventType, + MsgType, + RelationType, + UNSIGNED_THREAD_ID_FIELD, +} from "../@types/event"; import { Crypto } from "../crypto"; import { deepSortedObjectEntries, internaliseString } from "../utils"; import { RoomMember } from "./room-member"; @@ -63,6 +69,7 @@ export interface IUnsigned { "transaction_id"?: string; "invite_room_state"?: StrippedState[]; "m.relations"?: Record; // No common pattern for aggregated relations + [UNSIGNED_THREAD_ID_FIELD.name]?: string; } export interface IThreadBundledRelationship { diff --git a/src/models/invites-ignorer.ts b/src/models/invites-ignorer.ts index 173ba620d9b..bb18cf0797f 100644 --- a/src/models/invites-ignorer.ts +++ b/src/models/invites-ignorer.ts @@ -186,7 +186,7 @@ export class IgnoredInvites { } let regexp: RegExp; try { - regexp = new RegExp(globToRegexp(glob, false)); + regexp = new RegExp(globToRegexp(glob)); } catch (ex) { // Assume invalid event. continue; diff --git a/src/models/read-receipt.ts b/src/models/read-receipt.ts index 5858fe5bb99..1699779150a 100644 --- a/src/models/read-receipt.ts +++ b/src/models/read-receipt.ts @@ -20,7 +20,7 @@ import { WrappedReceipt, } from "../@types/read_receipts"; import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter"; -import * as utils from "../utils"; +import { isSupportedReceiptType } from "../utils"; import { MatrixEvent } from "./event"; import { EventType } from "../@types/event"; import { EventTimelineSet } from "./event-timeline-set"; @@ -267,7 +267,7 @@ export abstract class ReadReceipt< public getUsersReadUpTo(event: MatrixEvent): string[] { return this.getReceiptsForEvent(event) .filter(function (receipt) { - return utils.isSupportedReceiptType(receipt.type); + return isSupportedReceiptType(receipt.type); }) .map(function (receipt) { return receipt.userId; diff --git a/src/models/room-member.ts b/src/models/room-member.ts index 116a93b624b..e8fcfa60620 100644 --- a/src/models/room-member.ts +++ b/src/models/room-member.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { getHttpUriForMxc } from "../content-repo"; -import * as utils from "../utils"; +import { removeDirectionOverrideChars, removeHiddenChars } from "../utils"; import { User } from "./user"; import { MatrixEvent } from "./event"; import { RoomState } from "./room-state"; @@ -206,8 +206,8 @@ export class RoomMember extends TypedEventEmitter * @returns An array of user IDs or an empty array. */ public getUserIdsWithDisplayName(displayName: string): string[] { - return this.displayNameToUserIds.get(utils.removeHiddenChars(displayName)) ?? []; + return this.displayNameToUserIds.get(removeHiddenChars(displayName)) ?? []; } /** @@ -798,7 +798,7 @@ export class RoomState extends TypedEventEmitter } let requiredLevel = 50; - if (utils.isNumber(powerLevels[action])) { + if (isNumber(powerLevels[action])) { requiredLevel = powerLevels[action]!; } @@ -928,7 +928,7 @@ export class RoomState extends TypedEventEmitter powerLevelsEvent && powerLevelsEvent.getContent() && powerLevelsEvent.getContent().notifications && - utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey]) + isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey]) ) { notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey]; } @@ -1058,7 +1058,7 @@ export class RoomState extends TypedEventEmitter // We clobber the user_id > name lookup but the name -> [user_id] lookup // means we need to remove that user ID from that array rather than nuking // the lot. - const strippedOldName = utils.removeHiddenChars(oldName); + const strippedOldName = removeHiddenChars(oldName); const existingUserIds = this.displayNameToUserIds.get(strippedOldName); if (existingUserIds) { @@ -1070,7 +1070,7 @@ export class RoomState extends TypedEventEmitter this.userIdsToDisplayNames[userId] = displayName; - const strippedDisplayname = displayName && utils.removeHiddenChars(displayName); + const strippedDisplayname = displayName && removeHiddenChars(displayName); // an empty stripped displayname (undefined/'') will be set to MXID in room-member.js if (strippedDisplayname) { const arr = this.displayNameToUserIds.get(strippedDisplayname) ?? []; diff --git a/src/models/room.ts b/src/models/room.ts index 439bd681998..bd6913f45bb 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -24,7 +24,7 @@ import { } from "./event-timeline-set"; import { Direction, EventTimeline } from "./event-timeline"; import { getHttpUriForMxc } from "../content-repo"; -import * as utils from "../utils"; +import { compare, removeElement } from "../utils"; import { normalize, noUnsafeEventProps } from "../utils"; import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event"; import { EventStatus } from "./event-status"; @@ -39,6 +39,7 @@ import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS, EVENT_VISIBILITY_CHANGE_TYPE, RelationType, + UNSIGNED_THREAD_ID_FIELD, } from "../@types/event"; import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client"; import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials"; @@ -72,8 +73,8 @@ import { isPollEvent, Poll, PollEvent } from "./poll"; // room versions which are considered okay for people to run without being asked // to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers // return an m.room_versions capability. -export const KNOWN_SAFE_ROOM_VERSION = "9"; -const SAFE_ROOM_VERSIONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; +export const KNOWN_SAFE_ROOM_VERSION = "10"; +const SAFE_ROOM_VERSIONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]; interface IOpts { /** @@ -733,7 +734,7 @@ export class Room extends ReadReceipt { ); } - const removed = utils.removeElement( + const removed = removeElement( this.pendingEventList, function (ev) { return ev.getId() == eventId; @@ -809,7 +810,7 @@ export class Room extends ReadReceipt { const lastThreadEvent = lastThread.events[lastThread.events.length - 1]; - return (lastRoomEvent?.getTs() ?? 0) > (lastThreadEvent.getTs() ?? 0) ? lastRoomEvent : lastThreadEvent; + return (lastRoomEvent?.getTs() ?? 0) > (lastThreadEvent?.getTs() ?? 0) ? lastRoomEvent : lastThreadEvent; } /** @@ -2132,6 +2133,13 @@ export class Room extends ReadReceipt { return this.eventShouldLiveIn(parentEvent, events, roots); } + if (!event.isRelation()) { + return { + shouldLiveInRoom: true, + shouldLiveInThread: false, + }; + } + // Edge case where we know the event is a relation but don't have the parentEvent if (roots?.has(event.relationEventId!)) { return { @@ -2141,9 +2149,20 @@ export class Room extends ReadReceipt { }; } - // We've exhausted all scenarios, can safely assume that this event should live in the room timeline only + const unsigned = event.getUnsigned(); + if (typeof unsigned[UNSIGNED_THREAD_ID_FIELD.name] === "string") { + return { + shouldLiveInRoom: false, + shouldLiveInThread: true, + threadId: unsigned[UNSIGNED_THREAD_ID_FIELD.name], + }; + } + + // We've exhausted all scenarios, + // we cannot assume that it lives in the main timeline as this may be a relation for an unknown thread + // adding the event in the wrong timeline causes stuck notifications and can break ability to send read receipts return { - shouldLiveInRoom: true, + shouldLiveInRoom: false, shouldLiveInThread: false, }; } @@ -2156,14 +2175,13 @@ export class Room extends ReadReceipt { } private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void { - let thread = this.getThread(threadId); - - if (!thread) { + const thread = this.getThread(threadId); + if (thread) { + thread.addEvents(events, toStartOfTimeline); + } else { const rootEvent = this.findEventById(threadId) ?? events.find((e) => e.getId() === threadId); - thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline); + this.createThread(threadId, rootEvent, events, toStartOfTimeline); } - - thread.addEvents(events, toStartOfTimeline); } /** @@ -2700,16 +2718,20 @@ export class Room extends ReadReceipt { * @param addLiveEventOptions - addLiveEvent options * @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'. */ - public addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): void; + public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise; /** * @deprecated In favor of the overload with `IAddLiveEventOptions` */ - public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache?: boolean): void; - public addLiveEvents( + public async addLiveEvents( + events: MatrixEvent[], + duplicateStrategy?: DuplicateStrategy, + fromCache?: boolean, + ): Promise; + public async addLiveEvents( events: MatrixEvent[], duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions, fromCache = false, - ): void { + ): Promise { let duplicateStrategy: DuplicateStrategy | undefined = duplicateStrategyOrOpts as DuplicateStrategy; let timelineWasEmpty: boolean | undefined = false; if (typeof duplicateStrategyOrOpts === "object") { @@ -2760,6 +2782,9 @@ export class Room extends ReadReceipt { timelineWasEmpty, }; + // List of extra events to check for being parents of any relations encountered + const neighbouringEvents = [...events]; + for (const event of events) { // TODO: We should have a filter to say "only add state event types X Y Z to the timeline". this.processLiveEvent(event); @@ -2773,12 +2798,35 @@ export class Room extends ReadReceipt { } } - const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn( + let { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn( event, - events, + neighbouringEvents, threadRoots, ); + if (!shouldLiveInThread && !shouldLiveInRoom && event.isRelation()) { + try { + const parentEvent = new MatrixEvent( + await this.client.fetchRoomEvent(this.roomId, event.relationEventId!), + ); + neighbouringEvents.push(parentEvent); + if (parentEvent.threadRootId) { + threadRoots.add(parentEvent.threadRootId); + const unsigned = event.getUnsigned(); + unsigned[UNSIGNED_THREAD_ID_FIELD.name] = parentEvent.threadRootId; + event.setUnsigned(unsigned); + } + + ({ shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn( + event, + neighbouringEvents, + threadRoots, + )); + } catch (e) { + logger.error("Failed to load parent event of unhandled relation", e); + } + } + if (shouldLiveInThread && !eventsByThread[threadId ?? ""]) { eventsByThread[threadId ?? ""] = []; } @@ -2786,6 +2834,8 @@ export class Room extends ReadReceipt { if (shouldLiveInRoom) { this.addLiveEvent(event, options); + } else if (!shouldLiveInThread && event.isRelation()) { + this.relations.aggregateChildEvent(event); } } @@ -2796,13 +2846,14 @@ export class Room extends ReadReceipt { public partitionThreadedEvents( events: MatrixEvent[], - ): [timelineEvents: MatrixEvent[], threadedEvents: MatrixEvent[]] { + ): [timelineEvents: MatrixEvent[], threadedEvents: MatrixEvent[], unknownRelations: MatrixEvent[]] { // Indices to the events array, for readability const ROOM = 0; const THREAD = 1; + const UNKNOWN_RELATION = 2; if (this.client.supportsThreads()) { const threadRoots = this.findThreadRoots(events); - return events.reduce( + return events.reduce<[MatrixEvent[], MatrixEvent[], MatrixEvent[]]>( (memo, event: MatrixEvent) => { const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn( event, @@ -2819,13 +2870,17 @@ export class Room extends ReadReceipt { memo[THREAD].push(event); } + if (!shouldLiveInThread && !shouldLiveInRoom) { + memo[UNKNOWN_RELATION].push(event); + } + return memo; }, - [[] as MatrixEvent[], [] as MatrixEvent[]], + [[], [], []], ); } else { // When `experimentalThreadSupport` is disabled treat all events as timelineEvents - return [events as MatrixEvent[], [] as MatrixEvent[]]; + return [events as MatrixEvent[], [] as MatrixEvent[], [] as MatrixEvent[]]; } } @@ -2838,6 +2893,10 @@ export class Room extends ReadReceipt { if (event.isRelation(THREAD_RELATION_TYPE.name)) { threadRoots.add(event.relationEventId ?? ""); } + const unsigned = event.getUnsigned(); + if (typeof unsigned[UNSIGNED_THREAD_ID_FIELD.name] === "string") { + threadRoots.add(unsigned[UNSIGNED_THREAD_ID_FIELD.name]!); + } } return threadRoots; } @@ -3267,7 +3326,7 @@ export class Room extends ReadReceipt { return true; }); // make sure members have stable order - otherMembers.sort((a, b) => utils.compare(a.userId, b.userId)); + otherMembers.sort((a, b) => compare(a.userId, b.userId)); // only 5 first members, immitate summaryHeroes otherMembers = otherMembers.slice(0, 5); otherNames = otherMembers.map((m) => m.name); diff --git a/src/models/thread.ts b/src/models/thread.ts index c31499b2b34..3179860819d 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -203,11 +203,33 @@ export class Thread extends ReadReceipt { ): void => { // Add a synthesized receipt when paginating forward in the timeline if (!toStartOfTimeline) { - room!.addLocalEchoReceipt(event.getSender()!, event, ReceiptType.Read); + const sender = event.getSender(); + if (sender && room && this.shouldSendLocalEchoReceipt(sender, event)) { + room.addLocalEchoReceipt(sender, event, ReceiptType.Read); + } } this.onEcho(event, toStartOfTimeline ?? false); }; + private shouldSendLocalEchoReceipt(sender: string, event: MatrixEvent): boolean { + const recursionSupport = this.client.canSupport.get(Feature.RelationsRecursion) ?? ServerSupport.Unsupported; + + if (recursionSupport === ServerSupport.Unsupported) { + // Normally we add a local receipt, but if we don't have + // recursion support, then events may arrive out of order, so we + // only create a receipt if it's after our existing receipt. + const oldReceiptEventId = this.getReadReceiptForUserId(sender)?.eventId; + if (oldReceiptEventId) { + const receiptEvent = this.findEventById(oldReceiptEventId); + if (receiptEvent && receiptEvent.getTs() > event.getTs()) { + return false; + } + } + } + + return true; + } + private onLocalEcho = (event: MatrixEvent): void => { this.onEcho(event, false); }; @@ -236,6 +258,34 @@ export class Thread extends ReadReceipt { } } + /** + * TEMPORARY. Only call this when MSC3981 is not available, and we have some + * late-arriving events to insert, because we recursively found them as part + * of populating a thread. When we have MSC3981 we won't need it, because + * they will all be supplied by the homeserver in one request, and they will + * already be in the right order in that response. + * This is a copy of addEventToTimeline above, modified to call + * insertEventIntoTimeline so this event is inserted into our best guess of + * the right place based on timestamp. (We should be using Sync Order but we + * don't have it.) + * + * @internal + */ + public insertEventIntoTimeline(event: MatrixEvent): void { + const eventId = event.getId(); + if (!eventId) { + return; + } + // If the event is already in this thread, bail out + if (this.findEventById(eventId)) { + return; + } + this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState); + + // As far as we know, timeline should always be the same as events + this.timeline = this.events; + } + public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void { events.forEach((ev) => this.addEvent(ev, toStartOfTimeline, false)); this.updateThreadMetadata(); @@ -281,7 +331,14 @@ export class Thread extends ReadReceipt { */ this.replayEvents?.push(event); } else { - this.addEventToTimeline(event, toStartOfTimeline); + const recursionSupport = + this.client.canSupport.get(Feature.RelationsRecursion) ?? ServerSupport.Unsupported; + + if (recursionSupport === ServerSupport.Unsupported) { + this.insertEventIntoTimeline(event); + } else { + this.addEventToTimeline(event, toStartOfTimeline); + } } // Apply annotations and replace relations to the relations of the timeline only this.timelineSet.relations?.aggregateParentEvent(event); @@ -460,25 +517,28 @@ export class Thread extends ReadReceipt { // XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084 private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise { const recursionSupport = this.client.canSupport.get(Feature.RelationsRecursion) ?? ServerSupport.Unsupported; - if (recursionSupport !== ServerSupport.Unsupported) { + if (recursionSupport === ServerSupport.Unsupported) { return Promise.all( - events - .filter((e) => e.isEncrypted()) - .map((event: MatrixEvent) => { - if (event.isRelation()) return; // skip - relations don't get edits - return this.client - .relations(this.roomId, event.getId()!, RelationType.Replace, event.getType(), { + events.filter(isAnEncryptedThreadMessage).map(async (event: MatrixEvent) => { + try { + const relations = await this.client.relations( + this.roomId, + event.getId()!, + RelationType.Replace, + event.getType(), + { limit: 1, - }) - .then((relations) => { - if (relations.events.length) { - event.makeReplaced(relations.events[0]); - } - }) - .catch((e) => { - logger.error("Failed to load edits for encrypted thread event", e); - }); - }), + }, + ); + if (relations.events.length) { + const editEvent = relations.events[0]; + event.makeReplaced(editEvent); + this.insertEventIntoTimeline(editEvent); + } + } catch (e) { + logger.error("Failed to load edits for encrypted thread event", e); + } + }), ); } } @@ -648,6 +708,16 @@ export class Thread extends ReadReceipt { } } +/** + * Decide whether an event deserves to have its potential edits fetched. + * + * @returns true if this event is encrypted and is a message that is part of a + * thread - either inside it, or a root. + */ +function isAnEncryptedThreadMessage(event: MatrixEvent): boolean { + return event.isEncrypted() && (event.isRelation(THREAD_RELATION_TYPE.name) || event.isThreadRoot); +} + export const FILTER_RELATED_BY_SENDERS = new ServerControlledNamespacedValue( "related_by_senders", "io.element.relation_senders", diff --git a/src/models/typed-event-emitter.ts b/src/models/typed-event-emitter.ts index c359df5d18f..7eac48b962b 100644 --- a/src/models/typed-event-emitter.ts +++ b/src/models/typed-event-emitter.ts @@ -17,6 +17,7 @@ limitations under the License. // eslint-disable-next-line no-restricted-imports import { EventEmitter } from "events"; +/** Events emitted by EventEmitter itself */ export enum EventEmitterEvents { NewListener = "newListener", RemoveListener = "removeListener", @@ -24,10 +25,22 @@ export enum EventEmitterEvents { } type AnyListener = (...args: any) => any; + +/** Base class for types mapping from event name to the type of listeners to that event */ export type ListenerMap = { [eventName in E]: AnyListener }; + type EventEmitterEventListener = (eventName: string, listener: AnyListener) => void; type EventEmitterErrorListener = (error: Error) => void; +/** + * The expected type of a listener function for a particular event. + * + * Type parameters: + * * `E` - List of all events emitted by the `TypedEventEmitter`. Normally an enum type. + * * `A` - A type providing mappings from event names to listener types. + * * `T` - The name of the actual event that this listener is for. Normally one of the types in `E` or + * {@link EventEmitterEvents}. + */ export type Listener, T extends E | EventEmitterEvents> = T extends E ? A[T] : T extends EventEmitterEvents @@ -40,12 +53,20 @@ export type Listener, T extends E | E * This makes it much easier for us to distinguish between events, as we now need * to properly type this, so that our events are not stringly-based and prone * to silly typos. + * + * Type parameters: + * * `Events` - List of all events emitted by this `TypedEventEmitter`. Normally an enum type. + * * `Arguments` - A {@link ListenerMap} type providing mappings from event names to listener types. + * * `SuperclassArguments` - TODO: not really sure. Alternative listener mappings, I think? But only honoured for `.emit`? */ export class TypedEventEmitter< Events extends string, Arguments extends ListenerMap, SuperclassArguments extends ListenerMap = Arguments, > extends EventEmitter { + /** + * Alias for {@link TypedEventEmitter#on}. + */ public addListener( event: T, listener: Listener, @@ -53,32 +74,97 @@ export class TypedEventEmitter< return super.addListener(event, listener); } + /** + * Synchronously calls each of the listeners registered for the event named + * `event`, in the order they were registered, passing the supplied arguments + * to each. + * + * @param event - The name of the event to emit + * @param args - Arguments to pass to the listener + * @returns `true` if the event had listeners, `false` otherwise. + */ public emit(event: T, ...args: Parameters): boolean; public emit(event: T, ...args: Parameters): boolean; public emit(event: T, ...args: any[]): boolean { return super.emit(event, ...args); } + /** + * Returns the number of listeners listening to the event named `event`. + * + * @param event - The name of the event being listened for + */ public listenerCount(event: Events | EventEmitterEvents): number { return super.listenerCount(event); } - public listeners(event: Events | EventEmitterEvents): ReturnType { + /** + * Returns a copy of the array of listeners for the event named `event`. + */ + public listeners(event: Events | EventEmitterEvents): Function[] { return super.listeners(event); } + /** + * Alias for {@link TypedEventEmitter#removeListener} + */ public off(event: T, listener: Listener): this { return super.off(event, listener); } + /** + * Adds the `listener` function to the end of the listeners array for the + * event named `event`. + * + * No checks are made to see if the `listener` has already been added. Multiple calls + * passing the same combination of `event` and `listener` will result in the `listener` + * being added, and called, multiple times. + * + * By default, event listeners are invoked in the order they are added. The + * {@link TypedEventEmitter#prependListener} method can be used as an alternative to add the + * event listener to the beginning of the listeners array. + * + * @param event - The name of the event. + * @param listener - The callback function + * + * @returns a reference to the `EventEmitter`, so that calls can be chained. + */ public on(event: T, listener: Listener): this { return super.on(event, listener); } + /** + * Adds a **one-time** `listener` function for the event named `event`. The + * next time `event` is triggered, this listener is removed and then invoked. + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * + * By default, event listeners are invoked in the order they are added. + * The {@link TypedEventEmitter#prependOnceListener} method can be used as an alternative to add the + * event listener to the beginning of the listeners array. + * + * @param event - The name of the event. + * @param listener - The callback function + * + * @returns a reference to the `EventEmitter`, so that calls can be chained. + */ public once(event: T, listener: Listener): this { return super.once(event, listener); } + /** + * Adds the `listener` function to the _beginning_ of the listeners array for the + * event named `event`. + * + * No checks are made to see if the `listener` has already been added. Multiple calls + * passing the same combination of `event` and `listener` will result in the `listener` + * being added, and called, multiple times. + * + * @param event - The name of the event. + * @param listener - The callback function + * + * @returns a reference to the `EventEmitter`, so that calls can be chained. + */ public prependListener( event: T, listener: Listener, @@ -86,6 +172,15 @@ export class TypedEventEmitter< return super.prependListener(event, listener); } + /** + * Adds a **one-time**`listener` function for the event named `event` to the _beginning_ of the listeners array. + * The next time `event` is triggered, this listener is removed, and then invoked. + * + * @param event - The name of the event. + * @param listener - The callback function + * + * @returns a reference to the `EventEmitter`, so that calls can be chained. + */ public prependOnceListener( event: T, listener: Listener, @@ -93,10 +188,25 @@ export class TypedEventEmitter< return super.prependOnceListener(event, listener); } + /** + * Removes all listeners, or those of the specified `event`. + * + * It is bad practice to remove listeners added elsewhere in the code, + * particularly when the `EventEmitter` instance was created by some other + * component or module (e.g. sockets or file streams). + * + * @param event - The name of the event. If undefined, all listeners everywhere are removed. + * @returns a reference to the `EventEmitter`, so that calls can be chained. + */ public removeAllListeners(event?: Events | EventEmitterEvents): this { return super.removeAllListeners(event); } + /** + * Removes the specified `listener` from the listener array for the event named `event`. + * + * @returns a reference to the `EventEmitter`, so that calls can be chained. + */ public removeListener( event: T, listener: Listener, @@ -104,7 +214,11 @@ export class TypedEventEmitter< return super.removeListener(event, listener); } - public rawListeners(event: Events | EventEmitterEvents): ReturnType { + /** + * Returns a copy of the array of listeners for the event named `eventName`, + * including any wrappers (such as those created by `.once()`). + */ + public rawListeners(event: Events | EventEmitterEvents): Function[] { return super.rawListeners(event); } } diff --git a/src/receipt-accumulator.ts b/src/receipt-accumulator.ts index ce7230f0ca1..ded358ad9a1 100644 --- a/src/receipt-accumulator.ts +++ b/src/receipt-accumulator.ts @@ -18,7 +18,7 @@ import { IMinimalEvent } from "./sync-accumulator"; import { EventType } from "./@types/event"; import { isSupportedReceiptType, MapWithDefault, recursiveMapToObject } from "./utils"; import { IContent } from "./models/event"; -import { MAIN_ROOM_TIMELINE, ReceiptContent, ReceiptType } from "./@types/read_receipts"; +import { ReceiptContent, ReceiptType } from "./@types/read_receipts"; interface AccumulatedReceipt { data: IMinimalEvent; @@ -118,7 +118,27 @@ export class ReceiptAccumulator { eventId, }; - if (!data.thread_id || data.thread_id === MAIN_ROOM_TIMELINE) { + // In a world that supports threads, read receipts normally have + // a `thread_id` which is either the thread they belong in or + // `MAIN_ROOM_TIMELINE`, so we normally use `setThreaded(...)` + // here. The `MAIN_ROOM_TIMELINE` is just treated as another + // thread. + // + // We still encounter read receipts that are "unthreaded" + // (missing the `thread_id` property). These come from clients + // that don't support threads, and from threaded clients that + // are doing a "Mark room as read" operation. Unthreaded + // receipts mark everything "before" them as read, in all + // threads, where "before" means in Sync Order i.e. the order + // the events were received from the homeserver in a sync. + // [Note: we have some bugs where we use timestamp order instead + // of Sync Order, because we don't correctly remember the Sync + // Order. See #3325.] + // + // Calling the wrong method will cause incorrect behavior like + // messages re-appearing as "new" when you already read them + // previously. + if (!data.thread_id) { this.setUnthreaded(userId, receipt); } else { this.setThreaded(data.thread_id, userId, receipt); diff --git a/src/rust-crypto/CrossSigningIdentity.ts b/src/rust-crypto/CrossSigningIdentity.ts new file mode 100644 index 00000000000..27eb623100a --- /dev/null +++ b/src/rust-crypto/CrossSigningIdentity.ts @@ -0,0 +1,101 @@ +/* +Copyright 2023 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 { OlmMachine, CrossSigningStatus } from "@matrix-org/matrix-sdk-crypto-js"; + +import { BootstrapCrossSigningOpts } from "../crypto-api"; +import { logger } from "../logger"; +import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; +import { UIAuthCallback } from "../interactive-auth"; + +/** Manages the cross-signing keys for our own user. + */ +export class CrossSigningIdentity { + public constructor( + private readonly olmMachine: OlmMachine, + private readonly outgoingRequestProcessor: OutgoingRequestProcessor, + ) {} + + /** + * Initialise our cross-signing keys by creating new keys if they do not exist, and uploading to the server + */ + public async bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise { + if (opts.setupNewCrossSigning) { + await this.resetCrossSigning(opts.authUploadDeviceSigningKeys); + return; + } + + const olmDeviceStatus: CrossSigningStatus = await this.olmMachine.crossSigningStatus(); + const privateKeysInSecretStorage = false; // TODO + const olmDeviceHasKeys = + olmDeviceStatus.hasMaster && olmDeviceStatus.hasUserSigning && olmDeviceStatus.hasSelfSigning; + + // Log all relevant state for easier parsing of debug logs. + logger.log("bootStrapCrossSigning: starting", { + setupNewCrossSigning: opts.setupNewCrossSigning, + olmDeviceHasMaster: olmDeviceStatus.hasMaster, + olmDeviceHasUserSigning: olmDeviceStatus.hasUserSigning, + olmDeviceHasSelfSigning: olmDeviceStatus.hasSelfSigning, + privateKeysInSecretStorage, + }); + + if (!olmDeviceHasKeys && !privateKeysInSecretStorage) { + logger.log( + "bootStrapCrossSigning: Cross-signing private keys not found locally or in secret storage, creating new keys", + ); + await this.resetCrossSigning(opts.authUploadDeviceSigningKeys); + } else if (olmDeviceHasKeys) { + logger.log("bootStrapCrossSigning: Olm device has private keys: exporting to secret storage"); + await this.exportCrossSigningKeysToStorage(); + } else if (privateKeysInSecretStorage) { + logger.log( + "bootStrapCrossSigning: Cross-signing private keys not found locally, but they are available " + + "in secret storage, reading storage and caching locally", + ); + throw new Error("TODO"); + } + + // TODO: we might previously have bootstrapped cross-signing but not completed uploading the keys to the + // server -- in which case we should call OlmDevice.bootstrap_cross_signing. How do we know? + logger.log("bootStrapCrossSigning: complete"); + } + + /** Reset our cross-signing keys + * + * This method will: + * * Tell the OlmMachine to create new keys + * * Upload the new public keys and the device signature to the server + * * Upload the private keys to SSSS, if it is set up + */ + private async resetCrossSigning(authUploadDeviceSigningKeys?: UIAuthCallback): Promise { + const outgoingRequests: Array = await this.olmMachine.bootstrapCrossSigning(true); + + logger.log("bootStrapCrossSigning: publishing keys to server"); + for (const req of outgoingRequests) { + await this.outgoingRequestProcessor.makeOutgoingRequest(req, authUploadDeviceSigningKeys); + } + await this.exportCrossSigningKeysToStorage(); + } + + /** + * Extract the cross-signing keys from the olm machine and save them to secret storage, if it is configured + * + * (If secret storage is *not* configured, we assume that the export will happen when it is set up) + */ + private async exportCrossSigningKeysToStorage(): Promise { + // TODO + } +} diff --git a/src/rust-crypto/OutgoingRequestProcessor.ts b/src/rust-crypto/OutgoingRequestProcessor.ts index 7ac9a2105db..4e98bce8d49 100644 --- a/src/rust-crypto/OutgoingRequestProcessor.ts +++ b/src/rust-crypto/OutgoingRequestProcessor.ts @@ -23,11 +23,14 @@ import { RoomMessageRequest, SignatureUploadRequest, ToDeviceRequest, + SigningKeysUploadRequest, } from "@matrix-org/matrix-sdk-crypto-js"; import { logger } from "../logger"; import { IHttpOpts, MatrixHttpApi, Method } from "../http-api"; import { QueryDict } from "../utils"; +import { IAuthDict, UIAuthCallback } from "../interactive-auth"; +import { UIAResponse } from "../@types/uia"; /** * Common interface for all the request types returned by `OlmMachine.outgoingRequests`. @@ -53,7 +56,7 @@ export class OutgoingRequestProcessor { private readonly http: MatrixHttpApi, ) {} - public async makeOutgoingRequest(msg: OutgoingRequest): Promise { + public async makeOutgoingRequest(msg: OutgoingRequest, uiaCallback?: UIAuthCallback): Promise { let resp: string; /* refer https://docs.rs/matrix-sdk-crypto/0.6.0/matrix_sdk_crypto/requests/enum.OutgoingRequests.html @@ -79,6 +82,14 @@ export class OutgoingRequestProcessor { `/_matrix/client/v3/room/${encodeURIComponent(msg.room_id)}/send/` + `${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`; resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body); + } else if (msg instanceof SigningKeysUploadRequest) { + resp = await this.makeRequestWithUIA( + Method.Post, + "/_matrix/client/v3/keys/device_signing/upload", + {}, + msg.body, + uiaCallback, + ); } else { logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg)); resp = ""; @@ -89,6 +100,31 @@ export class OutgoingRequestProcessor { } } + private async makeRequestWithUIA( + method: Method, + path: string, + queryParams: QueryDict, + body: string, + uiaCallback: UIAuthCallback | undefined, + ): Promise { + if (!uiaCallback) { + return await this.rawJsonRequest(method, path, queryParams, body); + } + + const parsedBody = JSON.parse(body); + const makeRequest = async (auth: IAuthDict): Promise> => { + const newBody = { + ...parsedBody, + auth, + }; + const resp = await this.rawJsonRequest(method, path, queryParams, JSON.stringify(newBody)); + return JSON.parse(resp) as T; + }; + + const resp = await uiaCallback(makeRequest); + return JSON.stringify(resp); + } + private async rawJsonRequest(method: Method, path: string, queryParams: QueryDict, body: string): Promise { const opts = { // inhibit the JSON stringification and parsing within HttpApi. diff --git a/src/rust-crypto/index.ts b/src/rust-crypto/index.ts index fcb253e743a..80370b3fbe9 100644 --- a/src/rust-crypto/index.ts +++ b/src/rust-crypto/index.ts @@ -20,11 +20,22 @@ import { RustCrypto } from "./rust-crypto"; import { logger } from "../logger"; import { RUST_SDK_STORE_PREFIX } from "./constants"; import { IHttpOpts, MatrixHttpApi } from "../http-api"; - +import { ServerSideSecretStorage } from "../secret-storage"; + +/** + * Create a new `RustCrypto` implementation + * + * @param http - Low-level HTTP interface: used to make outgoing requests required by the rust SDK. + * We expect it to set the access token, etc. + * @param userId - The local user's User ID. + * @param deviceId - The local user's Device ID. + * @param secretStorage - Interface to server-side secret storage. + */ export async function initRustCrypto( http: MatrixHttpApi, userId: string, deviceId: string, + secretStorage: ServerSideSecretStorage, ): Promise { // initialise the rust matrix-sdk-crypto-js, if it hasn't already been done await RustSdkCryptoJs.initAsync(); @@ -38,7 +49,7 @@ export async function initRustCrypto( // TODO: use the pickle key for the passphrase const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass"); - const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId); + const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage); await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) => rustCrypto.onRoomKeysUpdated(sessions), ); diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index e516e199fd3..acfb82dd68f 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -30,10 +30,13 @@ import { RoomEncryptor } from "./RoomEncryptor"; import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; import { KeyClaimManager } from "./KeyClaimManager"; import { MapWithDefault } from "../utils"; -import { DeviceVerificationStatus } from "../crypto-api"; +import { BootstrapCrossSigningOpts, DeviceVerificationStatus } from "../crypto-api"; import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter"; import { IDownloadKeyResult, IQueryKeysRequest } from "../client"; import { Device, DeviceMap } from "../models/device"; +import { ServerSideSecretStorage } from "../secret-storage"; +import { CrossSigningKey } from "../crypto/api"; +import { CrossSigningIdentity } from "./CrossSigningIdentity"; /** * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. @@ -54,16 +57,32 @@ export class RustCrypto implements CryptoBackend { private eventDecryptor: EventDecryptor; private keyClaimManager: KeyClaimManager; private outgoingRequestProcessor: OutgoingRequestProcessor; + private crossSigningIdentity: CrossSigningIdentity; public constructor( + /** The `OlmMachine` from the underlying rust crypto sdk. */ private readonly olmMachine: RustSdkCryptoJs.OlmMachine, + + /** + * Low-level HTTP interface: used to make outgoing requests required by the rust SDK. + * + * We expect it to set the access token, etc. + */ private readonly http: MatrixHttpApi, + + /** The local user's User ID. */ _userId: string, + + /** The local user's Device ID. */ _deviceId: string, + + /** Interface to server-side secret storage */ + _secretStorage: ServerSideSecretStorage, ) { this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http); this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor); this.eventDecryptor = new EventDecryptor(olmMachine); + this.crossSigningIdentity = new CrossSigningIdentity(olmMachine, this.outgoingRequestProcessor); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -302,6 +321,35 @@ export class RustCrypto implements CryptoBackend { }); } + /** + * Implementation of {@link CryptoApi#isCrossSigningReady} + */ + public async isCrossSigningReady(): Promise { + return false; + } + + /** + * Implementation of {@link CryptoApi#getCrossSigningKeyId} + */ + public async getCrossSigningKeyId(type: CrossSigningKey = CrossSigningKey.Master): Promise { + // TODO + return null; + } + + /** + * Implementation of {@link CryptoApi#boostrapCrossSigning} + */ + public async bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise { + await this.crossSigningIdentity.bootstrapCrossSigning(opts); + } + + /** + * Implementation of {@link CryptoApi#isSecretStorageReady} + */ + public async isSecretStorageReady(): Promise { + return false; + } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // SyncCryptoCallbacks implementation @@ -319,7 +367,7 @@ export class RustCrypto implements CryptoBackend { private async receiveSyncChanges({ events, oneTimeKeysCounts = new Map(), - unusedFallbackKeys = new Set(), + unusedFallbackKeys, devices = new RustSdkCryptoJs.DeviceLists(), }: { events?: IToDeviceEvent[]; diff --git a/src/scheduler.ts b/src/scheduler.ts index e41770249c6..41612f1c902 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -18,11 +18,10 @@ limitations under the License. * This is an internal module which manages queuing, scheduling and retrying * of requests. */ -import * as utils from "./utils"; import { logger } from "./logger"; import { MatrixEvent } from "./models/event"; import { EventType } from "./@types/event"; -import { IDeferred } from "./utils"; +import { defer, IDeferred, removeElement } from "./utils"; import { ConnectionError, MatrixError } from "./http-api"; import { ISendEventResponse } from "./@types/requests"; @@ -175,7 +174,7 @@ export class MatrixScheduler { return false; } let removed = false; - utils.removeElement(this.queues[name], (element) => { + removeElement(this.queues[name], (element) => { if (element.event.getId() === event.getId()) { // XXX we should probably reject the promise? // https://github.com/matrix-org/matrix-js-sdk/issues/496 @@ -214,15 +213,15 @@ export class MatrixScheduler { if (!this.queues[queueName]) { this.queues[queueName] = []; } - const defer = utils.defer(); + const deferred = defer(); this.queues[queueName].push({ event: event, - defer: defer, + defer: deferred, attempts: 0, }); debuglog("Queue algorithm dumped event %s into queue '%s'", event.getId(), queueName); this.startProcessingQueues(); - return defer.promise; + return deferred.promise; } private startProcessingQueues(): void { diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index e0aa627e273..27eae2d94b9 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -17,7 +17,7 @@ limitations under the License. import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend"; import { NotificationCountType, Room, RoomEvent } from "./models/room"; import { logger } from "./logger"; -import * as utils from "./utils"; +import { promiseMapSeries } from "./utils"; import { EventTimeline } from "./models/event-timeline"; import { ClientEvent, IStoredClientOpts, MatrixClient } from "./client"; import { @@ -628,7 +628,7 @@ export class SlidingSyncSdk { if (roomData.invite_state) { const inviteStateEvents = mapEvents(this.client, room.roomId, roomData.invite_state); - this.injectRoomEvents(room, inviteStateEvents); + await this.injectRoomEvents(room, inviteStateEvents); if (roomData.initial) { room.recalculate(); this.client.store.storeRoom(room); @@ -700,7 +700,7 @@ export class SlidingSyncSdk { } } */ - this.injectRoomEvents(room, stateEvents, timelineEvents, roomData.num_live); + await this.injectRoomEvents(room, stateEvents, timelineEvents, roomData.num_live); // we deliberately don't add ephemeral events to the timeline room.addEphemeralEvents(ephemeralEvents); @@ -726,8 +726,8 @@ export class SlidingSyncSdk { } }; - await utils.promiseMapSeries(stateEvents, processRoomEvent); - await utils.promiseMapSeries(timelineEvents, processRoomEvent); + await promiseMapSeries(stateEvents, processRoomEvent); + await promiseMapSeries(timelineEvents, processRoomEvent); ephemeralEvents.forEach(function (e) { client.emit(ClientEvent.Event, e); }); @@ -747,12 +747,12 @@ export class SlidingSyncSdk { * @param numLive - the number of events in timelineEventList which just happened, * supplied from the server. */ - public injectRoomEvents( + public async injectRoomEvents( room: Room, stateEventList: MatrixEvent[], timelineEventList?: MatrixEvent[], numLive?: number, - ): void { + ): Promise { timelineEventList = timelineEventList || []; stateEventList = stateEventList || []; numLive = numLive || 0; @@ -811,11 +811,11 @@ export class SlidingSyncSdk { // 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, { + await room.addLiveEvents(timelineEventList, { fromCache: true, }); if (liveTimelineEvents.length > 0) { - room.addLiveEvents(liveTimelineEvents, { + await room.addLiveEvents(liveTimelineEvents, { fromCache: false, }); } diff --git a/src/store/index.ts b/src/store/index.ts index 78741786ac2..ac0a344e852 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -245,4 +245,9 @@ export interface IStore { * Removes a specific batch of to-device messages from the queue */ removeToDeviceBatch(id: number): Promise; + + /** + * Stop the store and perform any appropriate cleanup + */ + destroy(): Promise; } diff --git a/src/store/indexeddb-backend.ts b/src/store/indexeddb-backend.ts index 008867dfc33..c93afb9e705 100644 --- a/src/store/indexeddb-backend.ts +++ b/src/store/indexeddb-backend.ts @@ -35,6 +35,7 @@ export interface IIndexedDBBackend { saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise; getOldestToDeviceBatch(): Promise; removeToDeviceBatch(id: number): Promise; + destroy(): Promise; } export type UserTuple = [userId: string, presenceEvent: Partial]; diff --git a/src/store/indexeddb-local-backend.ts b/src/store/indexeddb-local-backend.ts index 80fed44c5c0..3bc5914066a 100644 --- a/src/store/indexeddb-local-backend.ts +++ b/src/store/indexeddb-local-backend.ts @@ -15,8 +15,8 @@ limitations under the License. */ import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../sync-accumulator"; -import * as utils from "../utils"; -import * as IndexedDBHelpers from "../indexeddb-helpers"; +import { deepCopy, promiseTry } from "../utils"; +import { exists as idbExists } from "../indexeddb-helpers"; import { logger } from "../logger"; import { IStateEventWithRoomId, IStoredClientOpts } from "../matrix"; import { ISavedSync } from "./index"; @@ -122,7 +122,7 @@ function reqAsCursorPromise(req: IDBRequest): Promise { export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { public static exists(indexedDB: IDBFactory, dbName: string): Promise { dbName = "matrix-js-sdk:" + (dbName || "default"); - return IndexedDBHelpers.exists(indexedDB, dbName); + return idbExists(indexedDB, dbName); } private readonly dbName: string; @@ -380,7 +380,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { if (copy) { // We must deep copy the stored data so that the /sync processing code doesn't // corrupt the internal state of the sync accumulator (it adds non-clonable keys) - return Promise.resolve(utils.deepCopy(data)); + return Promise.resolve(deepCopy(data)); } else { return Promise.resolve(data); } @@ -435,7 +435,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { */ private persistSyncData(nextBatch: string, roomsData: ISyncResponse["rooms"]): Promise { logger.log("Persisting sync data up to", nextBatch); - return utils.promiseTry(() => { + return promiseTry(() => { const txn = this.db!.transaction(["sync"], "readwrite"); const store = txn.objectStore("sync"); store.put({ @@ -456,7 +456,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * @returns Promise which resolves if the events were persisted. */ private persistAccountData(accountData: IMinimalEvent[]): Promise { - return utils.promiseTry(() => { + return promiseTry(() => { const txn = this.db!.transaction(["accountData"], "readwrite"); const store = txn.objectStore("accountData"); for (const event of accountData) { @@ -475,7 +475,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * @returns Promise which resolves if the users were persisted. */ private persistUserPresenceEvents(tuples: UserTuple[]): Promise { - return utils.promiseTry(() => { + return promiseTry(() => { const txn = this.db!.transaction(["users"], "readwrite"); const store = txn.objectStore("users"); for (const tuple of tuples) { @@ -495,7 +495,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * @returns A list of presence events in their raw form. */ public getUserPresenceEvents(): Promise { - return utils.promiseTry(() => { + return promiseTry(() => { const txn = this.db!.transaction(["users"], "readonly"); const store = txn.objectStore("users"); return selectQuery(store, undefined, (cursor) => { @@ -510,7 +510,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { */ private loadAccountData(): Promise { logger.log(`LocalIndexedDBStoreBackend: loading account data...`); - return utils.promiseTry(() => { + return promiseTry(() => { const txn = this.db!.transaction(["accountData"], "readonly"); const store = txn.objectStore("accountData"); return selectQuery(store, undefined, (cursor) => { @@ -528,7 +528,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { */ private loadSyncData(): Promise { logger.log(`LocalIndexedDBStoreBackend: loading sync data...`); - return utils.promiseTry(() => { + return promiseTry(() => { const txn = this.db!.transaction(["sync"], "readonly"); const store = txn.objectStore("sync"); return selectQuery(store, undefined, (cursor) => { @@ -594,4 +594,11 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { store.delete(id); await txnAsPromise(txn); } + + /* + * Close the database + */ + public async destroy(): Promise { + this.db?.close(); + } } diff --git a/src/store/indexeddb-remote-backend.ts b/src/store/indexeddb-remote-backend.ts index 7e2aa0ccbe9..32ff51efe71 100644 --- a/src/store/indexeddb-remote-backend.ts +++ b/src/store/indexeddb-remote-backend.ts @@ -200,4 +200,11 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { logger.warn("Unrecognised message from worker: ", msg); } }; + + /* + * Destroy the web worker + */ + public async destroy(): Promise { + this.worker?.terminate(); + } } diff --git a/src/store/indexeddb.ts b/src/store/indexeddb.ts index cc77bf9c80f..3ca9ea2dca4 100644 --- a/src/store/indexeddb.ts +++ b/src/store/indexeddb.ts @@ -151,6 +151,13 @@ export class IndexedDBStore extends MemoryStore { }); } + /* + * Close the database and destroy any associated workers + */ + public destroy(): Promise { + return this.backend.destroy(); + } + private onClose = (): void => { this.emitter.emit("closed"); }; diff --git a/src/store/memory.ts b/src/store/memory.ts index 091dcd4c8ca..8b560784622 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -435,4 +435,8 @@ export class MemoryStore implements IStore { this.pendingToDeviceBatches = this.pendingToDeviceBatches.filter((batch) => batch.id !== id); return Promise.resolve(); } + + public async destroy(): Promise { + // Nothing to do + } } diff --git a/src/store/stub.ts b/src/store/stub.ts index cc128540630..5ea91cf98cc 100644 --- a/src/store/stub.ts +++ b/src/store/stub.ts @@ -266,4 +266,8 @@ export class StubStore implements IStore { public async removeToDeviceBatch(id: number): Promise { return Promise.resolve(); } + + public async destroy(): Promise { + // Nothing to do + } } diff --git a/src/sync-accumulator.ts b/src/sync-accumulator.ts index ae6ef13b79b..e25ace53039 100644 --- a/src/sync-accumulator.ts +++ b/src/sync-accumulator.ts @@ -399,9 +399,9 @@ export class SyncAccumulator { const acc = currentData._summary; const sum = data.summary; - acc[HEROES_KEY] = sum[HEROES_KEY] || acc[HEROES_KEY]; - acc[JOINED_COUNT_KEY] = sum[JOINED_COUNT_KEY] || acc[JOINED_COUNT_KEY]; - acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] || acc[INVITED_COUNT_KEY]; + acc[HEROES_KEY] = sum[HEROES_KEY] ?? acc[HEROES_KEY]; + acc[JOINED_COUNT_KEY] = sum[JOINED_COUNT_KEY] ?? acc[JOINED_COUNT_KEY]; + acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] ?? acc[INVITED_COUNT_KEY]; } // We purposefully do not persist m.typing events. diff --git a/src/sync.ts b/src/sync.ts index 8bf0f8626fc..4c78aea89a0 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -28,7 +28,7 @@ import { Optional } from "matrix-events-sdk"; import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend"; import { User, UserEvent } from "./models/user"; import { NotificationCountType, Room, RoomEvent } from "./models/room"; -import * as utils from "./utils"; +import { promiseMapSeries, defer, deepCopy } from "./utils"; import { IDeferred, noUnsafeEventProps, unsafeProp } from "./utils"; import { Filter } from "./filter"; import { EventTimeline } from "./models/event-timeline"; @@ -414,7 +414,7 @@ export class SyncApi { // FIXME: Mostly duplicated from injectRoomEvents but not entirely // because "state" in this API is at the BEGINNING of the chunk - const oldStateEvents = utils.deepCopy(response.state).map(client.getEventMapper()); + const oldStateEvents = deepCopy(response.state).map(client.getEventMapper()); const stateEvents = response.state.map(client.getEventMapper()); const messages = response.messages.chunk.map(client.getEventMapper()); @@ -501,7 +501,7 @@ export class SyncApi { }, ) .then( - (res) => { + async (res) => { if (this._peekRoom !== peekRoom) { debuglog("Stopped peeking in room %s", peekRoom.roomId); return; @@ -541,7 +541,7 @@ export class SyncApi { }) .map(this.client.getEventMapper()); - peekRoom.addLiveEvents(events); + await peekRoom.addLiveEvents(events); this.peekPoll(peekRoom, res.end); }, (err) => { @@ -899,8 +899,6 @@ export class SyncApi { // Reset after a successful sync this.failedSyncCount = 0; - await this.client.store.setSyncData(data); - const syncEventData = { oldSyncToken: syncToken ?? undefined, nextSyncToken: data.next_batch, @@ -924,6 +922,10 @@ export class SyncApi { this.client.emit(ClientEvent.SyncUnexpectedError, e); } + // Persist after processing as `unsigned` may get mutated + // with an `org.matrix.msc4023.thread_id` + await this.client.store.setSyncData(data); + // update this as it may have changed syncEventData.catchingUp = this.catchingUp; @@ -1247,7 +1249,7 @@ export class SyncApi { this.notifEvents = []; // Handle invites - await utils.promiseMapSeries(inviteRooms, async (inviteObj) => { + await promiseMapSeries(inviteRooms, async (inviteObj) => { const room = inviteObj.room; const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room); @@ -1288,7 +1290,7 @@ export class SyncApi { }); // Handle joins - await utils.promiseMapSeries(joinRooms, async (joinObj) => { + await promiseMapSeries(joinRooms, async (joinObj) => { const room = joinObj.room; const stateEvents = this.mapSyncEventsFormat(joinObj.state, room); // Prevent events from being decrypted ahead of time @@ -1471,7 +1473,7 @@ export class SyncApi { }); // Handle leaves (e.g. kicked rooms) - await utils.promiseMapSeries(leaveRooms, async (leaveObj) => { + await promiseMapSeries(leaveRooms, async (leaveObj) => { const room = leaveObj.room; const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room); const events = this.mapSyncEventsFormat(leaveObj.timeline, room); @@ -1552,7 +1554,7 @@ export class SyncApi { this.pokeKeepAlive(); } if (!this.connectionReturnedDefer) { - this.connectionReturnedDefer = utils.defer(); + this.connectionReturnedDefer = defer(); } return this.connectionReturnedDefer.promise; } @@ -1627,16 +1629,17 @@ export class SyncApi { return Object.keys(obj) .filter((k) => !unsafeProp(k)) .map((roomId) => { - const arrObj = obj[roomId] as T & { room: Room; isBrandNewRoom: boolean }; let room = client.store.getRoom(roomId); let isBrandNewRoom = false; if (!room) { room = this.createRoom(roomId); isBrandNewRoom = true; } - arrObj.room = room; - arrObj.isBrandNewRoom = isBrandNewRoom; - return arrObj; + return { + ...obj[roomId], + room, + isBrandNewRoom, + }; }); } @@ -1773,7 +1776,7 @@ export class SyncApi { // 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 || [], { + await room.addLiveEvents(timelineEventList || [], { fromCache, timelineWasEmpty, }); diff --git a/src/utils.ts b/src/utils.ts index e2b29bdd4d1..1dba236c9c5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -357,27 +357,14 @@ export function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -export function globToRegexp(glob: string, extended = false): string { - // From - // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132 - // Because micromatch is about 130KB with dependencies, - // and minimatch is not much better. - const replacements: [RegExp, string | ((substring: string, ...args: any[]) => string)][] = [ - [/\\\*/g, ".*"], - [/\?/g, "."], - ]; - if (!extended) { - replacements.push([ - /\\\[(!|)(.*)\\]/g, - (_match: string, neg: string, pat: string): string => - ["[", neg ? "^" : "", pat.replace(/\\-/, "-"), "]"].join(""), - ]); - } - return replacements.reduce( - // https://github.com/microsoft/TypeScript/issues/30134 - (pat, args) => (args ? pat.replace(args[0], args[1] as any) : pat), - escapeRegExp(glob), - ); +/** + * Converts Matrix glob-style string to a regular expression + * https://spec.matrix.org/v1.7/appendices/#glob-style-matching + * @param glob - Matrix glob-style string + * @returns regular expression + */ +export function globToRegexp(glob: string): string { + return escapeRegExp(glob).replace(/\\\*/g, ".*").replace(/\?/g, "."); } export function ensureNoTrailingSlash(url: string): string; diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 641d4cb982e..875dd26996b 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -25,7 +25,7 @@ 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 { checkObjectHasKeys, isNullOrUndefined, recursivelyAssign } from "../utils"; import { IContent, MatrixEvent } from "../models/event"; import { EventType, ToDeviceMessageId } from "../@types/event"; import { RoomMember } from "../models/room-member"; @@ -263,7 +263,8 @@ const CALL_TIMEOUT_MS = 60 * 1000; // ms 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 - +/** The time after which we try a ICE restart, if ICE got disconnected */ +const ICE_RECONNECTING_TIMEOUT = 2 * 1000; // ms export class CallError extends Error { public readonly code: string; @@ -382,6 +383,7 @@ export class MatrixCall extends TypedEventEmitter; + private iceReconnectionTimeOut?: ReturnType | undefined; private inviteTimeout?: ReturnType; private readonly removeTrackListeners = new Map void>(); @@ -451,7 +453,7 @@ export class MatrixCall extends TypedEventEmitter { if (event.candidate) { if (this.candidatesEnded) { - logger.warn( - `Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended - ignoring!`, - ); - return; + logger.warn(`Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended!`); } logger.debug(`Call ${this.callId} got local ICE ${event.candidate.sdpMid} ${event.candidate.candidate}`); @@ -1817,7 +1817,10 @@ export class MatrixCall extends TypedEventEmitter void) | null) { this.candidatesEnded = false; + logger.debug( + `Call ${this.callId} onIceConnectionStateChanged() ice restart (state=${this.peerConn?.iceConnectionState})`, + ); this.peerConn!.restartIce(); } else { logger.info( @@ -2257,7 +2271,19 @@ export class MatrixCall extends TypedEventEmitter { + this.candidatesEnded = false; + this.iceReconnectionTimeOut = setTimeout((): void => { + logger.info( + `Call ${this.callId} onIceConnectionStateChanged() ICE restarting because of ICE disconnected, (state=${this.peerConn?.iceConnectionState}, conn=${this.peerConn?.connectionState})`, + ); + if (this.peerConn?.restartIce as (() => void) | null) { + this.candidatesEnded = false; + this.peerConn!.restartIce(); + } + this.iceReconnectionTimeOut = undefined; + }, ICE_RECONNECTING_TIMEOUT); + + this.iceDisconnectedTimeout = setTimeout((): void => { logger.info( `Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE disconnected for too long)`, ); @@ -2822,7 +2848,9 @@ export class MatrixCall extends TypedEventEmitter { @@ -2887,6 +2918,11 @@ export class MatrixCall extends TypedEventEmitter this.emit(CallFeedEvent.ConnectedChanged, this.connected); } - private get hasAudioTrack(): boolean { + public get hasAudioTrack(): boolean { return this.stream.getAudioTracks().length > 0; } private updateStream(oldStream: MediaStream | null, newStream: MediaStream): void { if (newStream === oldStream) return; + const wasMeasuringVolumeActivity = this.measuringVolumeActivity; + if (oldStream) { oldStream.removeEventListener("addtrack", this.onAddTrack); this.measureVolumeActivity(false); @@ -145,6 +147,7 @@ export class CallFeed extends TypedEventEmitter if (this.hasAudioTrack) { this.initVolumeMeasuring(); + if (wasMeasuringVolumeActivity) this.measureVolumeActivity(true); } else { this.measureVolumeActivity(false); } diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index a6a629576f4..6d98586c92b 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -655,27 +655,9 @@ export class GroupCall extends TypedEventEmitter< `GroupCall ${this.groupCallId} setMicrophoneMuted() (streamId=${this.localCallFeed.stream.id}, muted=${muted})`, ); - // We needed this here to avoid an error in case user join a call without a device. - // I can not use .then .catch functions because linter :-( - try { - if (!muted) { - const stream = await this.client - .getMediaHandler() - .getUserMediaStream(true, !this.localCallFeed.isVideoMuted()); - if (stream === null) { - // if case permission denied to get a stream stop this here - /* istanbul ignore next */ - logger.log( - `GroupCall ${this.groupCallId} setMicrophoneMuted() no device to receive local stream, muted=${muted}`, - ); - return false; - } - } - } catch (e) { - /* istanbul ignore next */ - logger.log( - `GroupCall ${this.groupCallId} setMicrophoneMuted() no device or permission to receive local stream, muted=${muted}`, - ); + const hasPermission = await this.checkAudioPermissionIfNecessary(muted); + + if (!hasPermission) { return false; } @@ -700,6 +682,42 @@ export class GroupCall extends TypedEventEmitter< return true; } + /** + * If we allow entering a call without a camera and without video, it can happen that the access rights to the + * devices have not yet been queried. If a stream does not yet have an audio track, we assume that the rights have + * not yet been checked. + * + * `this.client.getMediaHandler().getUserMediaStream` clones the current stream, so it only wanted to be called when + * not Audio Track exists. + * As such, this is a compromise, because, the access rights should always be queried before the call. + */ + private async checkAudioPermissionIfNecessary(muted: boolean): Promise { + // We needed this here to avoid an error in case user join a call without a device. + try { + if (!muted && this.localCallFeed && !this.localCallFeed.hasAudioTrack) { + const stream = await this.client + .getMediaHandler() + .getUserMediaStream(true, !this.localCallFeed.isVideoMuted()); + if (stream?.getTracks().length === 0) { + // if case permission denied to get a stream stop this here + /* istanbul ignore next */ + logger.log( + `GroupCall ${this.groupCallId} setMicrophoneMuted() no device to receive local stream, muted=${muted}`, + ); + return false; + } + } + } catch (e) { + /* istanbul ignore next */ + logger.log( + `GroupCall ${this.groupCallId} setMicrophoneMuted() no device or permission to receive local stream, muted=${muted}`, + ); + return false; + } + + return true; + } + /** * Sets the mute state of the local participants's video. * @param muted - Whether to mute the video diff --git a/src/webrtc/stats/statsReportGatherer.ts b/src/webrtc/stats/callStatsReportGatherer.ts similarity index 74% rename from src/webrtc/stats/statsReportGatherer.ts rename to src/webrtc/stats/callStatsReportGatherer.ts index 25f9b93c862..fb3e50e6907 100644 --- a/src/webrtc/stats/statsReportGatherer.ts +++ b/src/webrtc/stats/callStatsReportGatherer.ts @@ -17,17 +17,17 @@ limitations under the License. import { ConnectionStats } from "./connectionStats"; import { StatsReportEmitter } from "./statsReportEmitter"; import { ByteSend, ByteSentStatsReport, TrackID } from "./statsReport"; -import { ConnectionStatsReporter } from "./connectionStatsReporter"; -import { TransportStatsReporter } from "./transportStatsReporter"; +import { ConnectionStatsBuilder } from "./connectionStatsBuilder"; +import { TransportStatsBuilder } from "./transportStatsBuilder"; import { MediaSsrcHandler } from "./media/mediaSsrcHandler"; import { MediaTrackHandler } from "./media/mediaTrackHandler"; import { MediaTrackStatsHandler } from "./media/mediaTrackStatsHandler"; -import { TrackStatsReporter } from "./trackStatsReporter"; -import { StatsReportBuilder } from "./statsReportBuilder"; -import { StatsValueFormatter } from "./statsValueFormatter"; -import { SummaryStats } from "./summaryStats"; +import { TrackStatsBuilder } from "./trackStatsBuilder"; +import { ConnectionStatsReportBuilder } from "./connectionStatsReportBuilder"; +import { ValueFormatter } from "./valueFormatter"; +import { CallStatsReportSummary } from "./callStatsReportSummary"; -export class StatsReportGatherer { +export class CallStatsReportGatherer { private isActive = true; private previousStatsReport: RTCStatsReport | undefined; private currentStatsReport: RTCStatsReport | undefined; @@ -35,11 +35,9 @@ export class StatsReportGatherer { private readonly trackStats: MediaTrackStatsHandler; - // private readonly ssrcToMid = { local: new Map(), remote: new Map() }; - public constructor( public readonly callId: string, - public readonly remoteUserId: string, + private opponentMemberId: string, private readonly pc: RTCPeerConnection, private readonly emitter: StatsReportEmitter, private readonly isFocus = true, @@ -48,14 +46,15 @@ export class StatsReportGatherer { this.trackStats = new MediaTrackStatsHandler(new MediaSsrcHandler(), new MediaTrackHandler(pc)); } - public async processStats(groupCallId: string, localUserId: string): Promise { + public async processStats(groupCallId: string, localUserId: string): Promise { const summary = { + isFirstCollection: this.previousStatsReport === undefined, receivedMedia: 0, receivedAudioMedia: 0, receivedVideoMedia: 0, - audioTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0 }, - videoTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0 }, - } as SummaryStats; + audioTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0, concealedAudio: 0, totalAudio: 0 }, + videoTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0, concealedAudio: 0, totalAudio: 0 }, + } as CallStatsReportSummary; if (this.isActive) { const statsPromise = this.pc.getStats(); if (typeof statsPromise?.then === "function") { @@ -74,7 +73,7 @@ export class StatsReportGatherer { summary.receivedMedia = this.connectionStats.bitrate.download; summary.receivedAudioMedia = this.connectionStats.bitrate.audio?.download || 0; summary.receivedVideoMedia = this.connectionStats.bitrate.video?.download || 0; - const trackSummary = TrackStatsReporter.buildTrackSummary( + const trackSummary = TrackStatsBuilder.buildTrackSummary( Array.from(this.trackStats.getTrack2stats().values()), ); return { @@ -94,14 +93,16 @@ export class StatsReportGatherer { } private processStatsReport(groupCallId: string, localUserId: string): void { - const byteSentStats: ByteSentStatsReport = new Map(); + const byteSentStatsReport: ByteSentStatsReport = new Map() as ByteSentStatsReport; + byteSentStatsReport.callId = this.callId; + byteSentStatsReport.opponentMemberId = this.opponentMemberId; this.currentStatsReport?.forEach((now) => { const before = this.previousStatsReport ? this.previousStatsReport.get(now.id) : null; // RTCIceCandidatePairStats - https://w3c.github.io/webrtc-stats/#candidatepair-dict* if (now.type === "candidate-pair" && now.nominated && now.state === "succeeded") { - this.connectionStats.bandwidth = ConnectionStatsReporter.buildBandwidthReport(now); - this.connectionStats.transport = TransportStatsReporter.buildReport( + this.connectionStats.bandwidth = ConnectionStatsBuilder.buildBandwidthReport(now); + this.connectionStats.transport = TransportStatsBuilder.buildReport( this.currentStatsReport, now, this.connectionStats.transport, @@ -122,7 +123,7 @@ export class StatsReportGatherer { } if (before) { - TrackStatsReporter.buildPacketsLost(trackStats, now, before); + TrackStatsBuilder.buildPacketsLost(trackStats, now, before); } // Get the resolution and framerate for only remote video sources here. For the local video sources, @@ -131,25 +132,26 @@ export class StatsReportGatherer { // more calculations needed to determine what is the highest resolution stream sent by the client if the // 'outbound-rtp' stats are used. if (now.type === "inbound-rtp") { - TrackStatsReporter.buildFramerateResolution(trackStats, now); + TrackStatsBuilder.buildFramerateResolution(trackStats, now); if (before) { - TrackStatsReporter.buildBitrateReceived(trackStats, now, before); + TrackStatsBuilder.buildBitrateReceived(trackStats, now, before); } const ts = this.trackStats.findTransceiverByTrackId(trackStats.trackId); - TrackStatsReporter.setTrackStatsState(trackStats, ts); - TrackStatsReporter.buildJitter(trackStats, now); + TrackStatsBuilder.setTrackStatsState(trackStats, ts); + TrackStatsBuilder.buildJitter(trackStats, now); + TrackStatsBuilder.buildAudioConcealment(trackStats, now); } else if (before) { - byteSentStats.set(trackStats.trackId, StatsValueFormatter.getNonNegativeValue(now.bytesSent)); - TrackStatsReporter.buildBitrateSend(trackStats, now, before); + byteSentStatsReport.set(trackStats.trackId, ValueFormatter.getNonNegativeValue(now.bytesSent)); + TrackStatsBuilder.buildBitrateSend(trackStats, now, before); } - TrackStatsReporter.buildCodec(this.currentStatsReport, trackStats, now); + TrackStatsBuilder.buildCodec(this.currentStatsReport, trackStats, now); } else if (now.type === "track" && now.kind === "video" && !now.remoteSource) { const trackStats = this.trackStats.findLocalVideoTrackStats(now); if (!trackStats) { return; } - TrackStatsReporter.buildFramerateResolution(trackStats, now); - TrackStatsReporter.calculateSimulcastFramerate( + TrackStatsBuilder.buildFramerateResolution(trackStats, now); + TrackStatsBuilder.calculateSimulcastFramerate( trackStats, now, before, @@ -158,8 +160,8 @@ export class StatsReportGatherer { } }); - this.emitter.emitByteSendReport(byteSentStats); - this.processAndEmitReport(); + this.emitter.emitByteSendReport(byteSentStatsReport); + this.processAndEmitConnectionStatsReport(); } public setActive(isActive: boolean): void { @@ -174,8 +176,10 @@ export class StatsReportGatherer { this.isActive = false; } - private processAndEmitReport(): void { - const report = StatsReportBuilder.build(this.trackStats.getTrack2stats()); + private processAndEmitConnectionStatsReport(): void { + const report = ConnectionStatsReportBuilder.build(this.trackStats.getTrack2stats()); + report.callId = this.callId; + report.opponentMemberId = this.opponentMemberId; this.connectionStats.bandwidth = report.bandwidth; this.connectionStats.bitrate = report.bitrate; @@ -201,4 +205,8 @@ export class StatsReportGatherer { } } } + + public setOpponentMemberId(id: string): void { + this.opponentMemberId = id; + } } diff --git a/src/webrtc/stats/summaryStats.ts b/src/webrtc/stats/callStatsReportSummary.ts similarity index 87% rename from src/webrtc/stats/summaryStats.ts rename to src/webrtc/stats/callStatsReportSummary.ts index f708a5bad09..fc62ae3b3f1 100644 --- a/src/webrtc/stats/summaryStats.ts +++ b/src/webrtc/stats/callStatsReportSummary.ts @@ -10,12 +10,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -export interface SummaryStats { +export interface CallStatsReportSummary { receivedMedia: number; receivedAudioMedia: number; receivedVideoMedia: number; audioTrackSummary: TrackSummary; videoTrackSummary: TrackSummary; + + isFirstCollection: Boolean; } export interface TrackSummary { @@ -23,4 +25,6 @@ export interface TrackSummary { muted: number; maxJitter: number; maxPacketLoss: number; + concealedAudio: number; + totalAudio: number; } diff --git a/src/webrtc/stats/connectionStats.ts b/src/webrtc/stats/connectionStats.ts index dbde6e50327..ef1c36797ea 100644 --- a/src/webrtc/stats/connectionStats.ts +++ b/src/webrtc/stats/connectionStats.ts @@ -33,7 +33,7 @@ export interface ConnectionStatsBitrate extends Bitrate { video?: Bitrate; } -export interface PacketLoos { +export interface PacketLoss { total: number; download: number; upload: number; @@ -42,6 +42,6 @@ export interface PacketLoos { export class ConnectionStats { public bandwidth: ConnectionStatsBitrate = {} as ConnectionStatsBitrate; public bitrate: ConnectionStatsBitrate = {} as ConnectionStatsBitrate; - public packetLoss: PacketLoos = {} as PacketLoos; + public packetLoss: PacketLoss = {} as PacketLoss; public transport: TransportStats[] = []; } diff --git a/src/webrtc/stats/connectionStatsReporter.ts b/src/webrtc/stats/connectionStatsBuilder.ts similarity index 96% rename from src/webrtc/stats/connectionStatsReporter.ts rename to src/webrtc/stats/connectionStatsBuilder.ts index c43b9b40c19..f954fed49f7 100644 --- a/src/webrtc/stats/connectionStatsReporter.ts +++ b/src/webrtc/stats/connectionStatsBuilder.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { Bitrate } from "./media/mediaTrackStats"; -export class ConnectionStatsReporter { +export class ConnectionStatsBuilder { public static buildBandwidthReport(now: RTCIceCandidatePairStats): Bitrate { const availableIncomingBitrate = now.availableIncomingBitrate; const availableOutgoingBitrate = now.availableOutgoingBitrate; diff --git a/src/webrtc/stats/statsReportBuilder.ts b/src/webrtc/stats/connectionStatsReportBuilder.ts similarity index 76% rename from src/webrtc/stats/statsReportBuilder.ts rename to src/webrtc/stats/connectionStatsReportBuilder.ts index eeca4ed4074..5ed99d59bae 100644 --- a/src/webrtc/stats/statsReportBuilder.ts +++ b/src/webrtc/stats/connectionStatsReportBuilder.ts @@ -13,10 +13,10 @@ 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 { CodecMap, ConnectionStatsReport, FramerateMap, ResolutionMap, TrackID } from "./statsReport"; +import { AudioConcealment, CodecMap, ConnectionStatsReport, FramerateMap, ResolutionMap, TrackID } from "./statsReport"; import { MediaTrackStats, Resolution } from "./media/mediaTrackStats"; -export class StatsReportBuilder { +export class ConnectionStatsReportBuilder { public static build(stats: Map): ConnectionStatsReport { const report = {} as ConnectionStatsReport; @@ -38,12 +38,16 @@ export class StatsReportBuilder { const framerates: FramerateMap = { local: new Map(), remote: new Map() }; const codecs: CodecMap = { local: new Map(), remote: new Map() }; const jitter = new Map(); + const audioConcealment = new Map(); let audioBitrateDownload = 0; let audioBitrateUpload = 0; let videoBitrateDownload = 0; let videoBitrateUpload = 0; + let totalConcealedAudio = 0; + let totalAudioDuration = 0; + for (const [trackId, trackStats] of stats) { // process packet loss stats const loss = trackStats.getLoss(); @@ -58,6 +62,11 @@ export class StatsReportBuilder { // collect resolutions and framerates if (trackStats.kind === "audio") { + // process audio quality stats + const audioConcealmentForTrack = trackStats.getAudioConcealment(); + totalConcealedAudio += audioConcealmentForTrack.concealedAudio; + totalAudioDuration += audioConcealmentForTrack.totalAudioDuration; + audioBitrateDownload += trackStats.getBitrate().download; audioBitrateUpload += trackStats.getBitrate().upload; } else { @@ -70,6 +79,9 @@ export class StatsReportBuilder { codecs[trackStats.getType()].set(trackId, trackStats.getCodec()); if (trackStats.getType() === "remote") { jitter.set(trackId, trackStats.getJitter()); + if (trackStats.kind === "audio") { + audioConcealment.set(trackId, trackStats.getAudioConcealment()); + } } trackStats.resetBitrate(); @@ -91,13 +103,19 @@ export class StatsReportBuilder { }; report.packetLoss = { - total: StatsReportBuilder.calculatePacketLoss( + total: ConnectionStatsReportBuilder.calculatePacketLoss( lostPackets.download + lostPackets.upload, totalPackets.download + totalPackets.upload, ), - download: StatsReportBuilder.calculatePacketLoss(lostPackets.download, totalPackets.download), - upload: StatsReportBuilder.calculatePacketLoss(lostPackets.upload, totalPackets.upload), + download: ConnectionStatsReportBuilder.calculatePacketLoss(lostPackets.download, totalPackets.download), + upload: ConnectionStatsReportBuilder.calculatePacketLoss(lostPackets.upload, totalPackets.upload), }; + report.audioConcealment = audioConcealment; + report.totalAudioConcealment = { + concealedAudio: totalConcealedAudio, + totalAudioDuration, + }; + report.framerate = framerates; report.resolution = resolutions; report.codec = codecs; diff --git a/src/webrtc/stats/groupCallStats.ts b/src/webrtc/stats/groupCallStats.ts index 898424846fb..40ee9797a44 100644 --- a/src/webrtc/stats/groupCallStats.ts +++ b/src/webrtc/stats/groupCallStats.ts @@ -13,16 +13,16 @@ 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 { StatsReportGatherer } from "./statsReportGatherer"; +import { CallStatsReportGatherer } from "./callStatsReportGatherer"; import { StatsReportEmitter } from "./statsReportEmitter"; -import { SummaryStats } from "./summaryStats"; -import { SummaryStatsReporter } from "./summaryStatsReporter"; +import { CallStatsReportSummary } from "./callStatsReportSummary"; +import { SummaryStatsReportGatherer } from "./summaryStatsReportGatherer"; export class GroupCallStats { private timer: undefined | ReturnType; - private readonly gatherers: Map = new Map(); + private readonly gatherers: Map = new Map(); public readonly reports = new StatsReportEmitter(); - private readonly summaryStatsReporter = new SummaryStatsReporter(this.reports); + private readonly summaryStatsReportGatherer = new SummaryStatsReportGatherer(this.reports); public constructor(private groupCallId: string, private userId: string, private interval: number = 10000) {} @@ -45,11 +45,15 @@ export class GroupCallStats { return this.gatherers.has(callId); } - public addStatsReportGatherer(callId: string, userId: string, peerConnection: RTCPeerConnection): boolean { + public addStatsReportGatherer( + callId: string, + opponentMemberId: string, + peerConnection: RTCPeerConnection, + ): boolean { if (this.hasStatsReportGatherer(callId)) { return false; } - this.gatherers.set(callId, new StatsReportGatherer(callId, userId, peerConnection, this.reports)); + this.gatherers.set(callId, new CallStatsReportGatherer(callId, opponentMemberId, peerConnection, this.reports)); return true; } @@ -57,17 +61,21 @@ export class GroupCallStats { return this.gatherers.delete(callId); } - public getStatsReportGatherer(callId: string): StatsReportGatherer | undefined { + public getStatsReportGatherer(callId: string): CallStatsReportGatherer | undefined { return this.hasStatsReportGatherer(callId) ? this.gatherers.get(callId) : undefined; } + public updateOpponentMember(callId: string, opponentMember: string): void { + this.getStatsReportGatherer(callId)?.setOpponentMemberId(opponentMember); + } + private processStats(): void { - const summary: Promise[] = []; + const summary: Promise[] = []; this.gatherers.forEach((c) => { summary.push(c.processStats(this.groupCallId, this.userId)); }); - Promise.all(summary).then((s: Awaited[]) => this.summaryStatsReporter.build(s)); + Promise.all(summary).then((s: Awaited[]) => this.summaryStatsReportGatherer.build(s)); } public setInterval(interval: number): void { diff --git a/src/webrtc/stats/media/mediaTrackStats.ts b/src/webrtc/stats/media/mediaTrackStats.ts index 66475e19ce6..7835ceb8a65 100644 --- a/src/webrtc/stats/media/mediaTrackStats.ts +++ b/src/webrtc/stats/media/mediaTrackStats.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { AudioConcealment } from "../statsReport"; import { TrackId } from "./mediaTrackHandler"; export interface PacketLoss { @@ -32,7 +33,14 @@ export interface Bitrate { */ upload: number; } +export interface ConcealedAudio { + /** + * duration in ms + */ + duration: number; + ratio: number; +} export interface Resolution { width: number; height: number; @@ -44,6 +52,7 @@ export class MediaTrackStats { private loss: PacketLoss = { packetsTotal: 0, packetsLost: 0, isDownloadStream: false }; private bitrate: Bitrate = { download: 0, upload: 0 }; private resolution: Resolution = { width: -1, height: -1 }; + private audioConcealment: AudioConcealment = { concealedAudio: 0, totalAudioDuration: 0 }; private framerate = 0; private jitter = 0; private codec = ""; @@ -61,8 +70,8 @@ export class MediaTrackStats { return this.type; } - public setLoss(loos: PacketLoss): void { - this.loss = loos; + public setLoss(loss: PacketLoss): void { + this.loss = loss; } public getLoss(): PacketLoss { @@ -152,4 +161,16 @@ export class MediaTrackStats { public getJitter(): number { return this.jitter; } + + /** + * Audio concealment ration (conceled duration / total duration) + */ + public setAudioConcealment(concealedAudioDuration: number, totalAudioDuration: number): void { + this.audioConcealment.concealedAudio = concealedAudioDuration; + this.audioConcealment.totalAudioDuration = totalAudioDuration; + } + + public getAudioConcealment(): AudioConcealment { + return this.audioConcealment; + } } diff --git a/src/webrtc/stats/statsReport.ts b/src/webrtc/stats/statsReport.ts index 64bb6aba440..750c26051bb 100644 --- a/src/webrtc/stats/statsReport.ts +++ b/src/webrtc/stats/statsReport.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ConnectionStatsBandwidth, ConnectionStatsBitrate, PacketLoos } from "./connectionStats"; +import { ConnectionStatsBandwidth, ConnectionStatsBitrate, PacketLoss } from "./connectionStats"; import { TransportStats } from "./transportStats"; import { Resolution } from "./media/mediaTrackStats"; @@ -28,13 +28,19 @@ export type TrackID = string; export type ByteSend = number; export interface ByteSentStatsReport extends Map { + callId?: string; + opponentMemberId?: string; // is a map: `local trackID` => byte send } export interface ConnectionStatsReport { + callId?: string; + opponentMemberId?: string; bandwidth: ConnectionStatsBandwidth; bitrate: ConnectionStatsBitrate; - packetLoss: PacketLoos; + packetLoss: PacketLoss; + audioConcealment: Map; + totalAudioConcealment: AudioConcealment; resolution: ResolutionMap; framerate: FramerateMap; codec: CodecMap; @@ -42,6 +48,11 @@ export interface ConnectionStatsReport { transport: TransportStats[]; } +export interface AudioConcealment { + concealedAudio: number; + totalAudioDuration: number; +} + export interface ResolutionMap { local: Map; remote: Map; @@ -70,4 +81,6 @@ export interface SummaryStatsReport { percentageReceivedVideoMedia: number; maxJitter: number; maxPacketLoss: number; + percentageConcealedAudio: number; + peerConnections: number; } diff --git a/src/webrtc/stats/summaryStatsReportGatherer.ts b/src/webrtc/stats/summaryStatsReportGatherer.ts new file mode 100644 index 00000000000..87601dcac7a --- /dev/null +++ b/src/webrtc/stats/summaryStatsReportGatherer.ts @@ -0,0 +1,121 @@ +/* +Copyright 2023 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 { StatsReportEmitter } from "./statsReportEmitter"; +import { CallStatsReportSummary } from "./callStatsReportSummary"; +import { SummaryStatsReport } from "./statsReport"; + +interface CallStatsReportSummaryCounter { + receivedAudio: number; + receivedVideo: number; + receivedMedia: number; + concealedAudio: number; + totalAudio: number; +} + +export class SummaryStatsReportGatherer { + public constructor(private emitter: StatsReportEmitter) {} + + public build(allSummary: CallStatsReportSummary[]): void { + // Filter all stats which collect the first time webrtc stats. + // Because stats based on time interval and the first collection of a summery stats has no previous + // webrtcStats as basement all the calculation are 0. We don't want track the 0 stats. + const summary = allSummary.filter((s) => !s.isFirstCollection); + const summaryTotalCount = summary.length; + if (summaryTotalCount === 0) { + return; + } + const summaryCounter: CallStatsReportSummaryCounter = { + receivedAudio: 0, + receivedVideo: 0, + receivedMedia: 0, + concealedAudio: 0, + totalAudio: 0, + }; + let maxJitter = 0; + let maxPacketLoss = 0; + summary.forEach((stats) => { + this.countTrackListReceivedMedia(summaryCounter, stats); + this.countConcealedAudio(summaryCounter, stats); + maxJitter = this.buildMaxJitter(maxJitter, stats); + maxPacketLoss = this.buildMaxPacketLoss(maxPacketLoss, stats); + }); + const decimalPlaces = 5; + const report = { + percentageReceivedMedia: Number((summaryCounter.receivedMedia / summaryTotalCount).toFixed(decimalPlaces)), + percentageReceivedVideoMedia: Number( + (summaryCounter.receivedVideo / summaryTotalCount).toFixed(decimalPlaces), + ), + percentageReceivedAudioMedia: Number( + (summaryCounter.receivedAudio / summaryTotalCount).toFixed(decimalPlaces), + ), + maxJitter, + maxPacketLoss, + percentageConcealedAudio: Number( + summaryCounter.totalAudio > 0 + ? (summaryCounter.concealedAudio / summaryCounter.totalAudio).toFixed(decimalPlaces) + : 0, + ), + peerConnections: summaryTotalCount, + } as SummaryStatsReport; + this.emitter.emitSummaryStatsReport(report); + } + + private countTrackListReceivedMedia(counter: CallStatsReportSummaryCounter, stats: CallStatsReportSummary): void { + let hasReceivedAudio = false; + let hasReceivedVideo = false; + if (stats.receivedAudioMedia > 0 || stats.audioTrackSummary.count === 0) { + counter.receivedAudio++; + hasReceivedAudio = true; + } + if (stats.receivedVideoMedia > 0 || stats.videoTrackSummary.count === 0) { + counter.receivedVideo++; + hasReceivedVideo = true; + } else { + if (stats.videoTrackSummary.muted > 0 && stats.videoTrackSummary.muted === stats.videoTrackSummary.count) { + counter.receivedVideo++; + hasReceivedVideo = true; + } + } + + if (hasReceivedVideo && hasReceivedAudio) { + counter.receivedMedia++; + } + } + + private buildMaxJitter(maxJitter: number, stats: CallStatsReportSummary): number { + if (maxJitter < stats.videoTrackSummary.maxJitter) { + maxJitter = stats.videoTrackSummary.maxJitter; + } + + if (maxJitter < stats.audioTrackSummary.maxJitter) { + maxJitter = stats.audioTrackSummary.maxJitter; + } + return maxJitter; + } + + private buildMaxPacketLoss(maxPacketLoss: number, stats: CallStatsReportSummary): number { + if (maxPacketLoss < stats.videoTrackSummary.maxPacketLoss) { + maxPacketLoss = stats.videoTrackSummary.maxPacketLoss; + } + + if (maxPacketLoss < stats.audioTrackSummary.maxPacketLoss) { + maxPacketLoss = stats.audioTrackSummary.maxPacketLoss; + } + return maxPacketLoss; + } + + private countConcealedAudio(summaryCounter: CallStatsReportSummaryCounter, stats: CallStatsReportSummary): void { + summaryCounter.concealedAudio += stats.audioTrackSummary.concealedAudio; + summaryCounter.totalAudio += stats.audioTrackSummary.totalAudio; + } +} diff --git a/src/webrtc/stats/summaryStatsReporter.ts b/src/webrtc/stats/summaryStatsReporter.ts deleted file mode 100644 index 66f738b1ca5..00000000000 --- a/src/webrtc/stats/summaryStatsReporter.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* -Copyright 2023 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 { StatsReportEmitter } from "./statsReportEmitter"; -import { SummaryStats } from "./summaryStats"; -import { SummaryStatsReport } from "./statsReport"; - -interface ReceivedMedia { - audio: number; - video: number; - media: number; -} - -export class SummaryStatsReporter { - public constructor(private emitter: StatsReportEmitter) {} - - public build(summary: SummaryStats[]): void { - const entiretyTracksCount = summary.length; - if (entiretyTracksCount === 0) { - return; - } - const receivedCounter: ReceivedMedia = { audio: 0, video: 0, media: 0 }; - let maxJitter = 0; - let maxPacketLoss = 0; - - summary.forEach((stats) => { - this.countTrackListReceivedMedia(receivedCounter, stats); - maxJitter = this.buildMaxJitter(maxJitter, stats); - maxPacketLoss = this.buildMaxPacketLoss(maxPacketLoss, stats); - }); - - const report = { - percentageReceivedMedia: Math.round((receivedCounter.media / entiretyTracksCount) * 100) / 100, - percentageReceivedVideoMedia: Math.round((receivedCounter.video / entiretyTracksCount) * 100) / 100, - percentageReceivedAudioMedia: Math.round((receivedCounter.audio / entiretyTracksCount) * 100) / 100, - maxJitter, - maxPacketLoss, - } as SummaryStatsReport; - this.emitter.emitSummaryStatsReport(report); - } - - private countTrackListReceivedMedia(counter: ReceivedMedia, stats: SummaryStats): void { - let hasReceivedAudio = false; - let hasReceivedVideo = false; - if (stats.receivedAudioMedia > 0 || stats.audioTrackSummary.count === 0) { - counter.audio++; - hasReceivedAudio = true; - } - if (stats.receivedVideoMedia > 0 || stats.videoTrackSummary.count === 0) { - counter.video++; - hasReceivedVideo = true; - } else { - if (stats.videoTrackSummary.muted > 0 && stats.videoTrackSummary.muted === stats.videoTrackSummary.count) { - counter.video++; - hasReceivedVideo = true; - } - } - - if (hasReceivedVideo && hasReceivedAudio) { - counter.media++; - } - } - - private buildMaxJitter(maxJitter: number, stats: SummaryStats): number { - if (maxJitter < stats.videoTrackSummary.maxJitter) { - maxJitter = stats.videoTrackSummary.maxJitter; - } - - if (maxJitter < stats.audioTrackSummary.maxJitter) { - maxJitter = stats.audioTrackSummary.maxJitter; - } - return maxJitter; - } - - private buildMaxPacketLoss(maxPacketLoss: number, stats: SummaryStats): number { - if (maxPacketLoss < stats.videoTrackSummary.maxPacketLoss) { - maxPacketLoss = stats.videoTrackSummary.maxPacketLoss; - } - - if (maxPacketLoss < stats.audioTrackSummary.maxPacketLoss) { - maxPacketLoss = stats.audioTrackSummary.maxPacketLoss; - } - return maxPacketLoss; - } -} diff --git a/src/webrtc/stats/trackStatsReporter.ts b/src/webrtc/stats/trackStatsBuilder.ts similarity index 65% rename from src/webrtc/stats/trackStatsReporter.ts rename to src/webrtc/stats/trackStatsBuilder.ts index e243cb32603..c670fddbb47 100644 --- a/src/webrtc/stats/trackStatsReporter.ts +++ b/src/webrtc/stats/trackStatsBuilder.ts @@ -1,8 +1,8 @@ import { MediaTrackStats } from "./media/mediaTrackStats"; -import { StatsValueFormatter } from "./statsValueFormatter"; -import { TrackSummary } from "./summaryStats"; +import { ValueFormatter } from "./valueFormatter"; +import { TrackSummary } from "./callStatsReportSummary"; -export class TrackStatsReporter { +export class TrackStatsBuilder { public static buildFramerateResolution(trackStats: MediaTrackStats, now: any): void { const resolution = { height: now.frameHeight, @@ -56,7 +56,7 @@ export class TrackStatsReporter { public static buildBitrateReceived(trackStats: MediaTrackStats, now: any, before: any): void { trackStats.setBitrate({ - download: TrackStatsReporter.calculateBitrate( + download: TrackStatsBuilder.calculateBitrate( now.bytesReceived, before.bytesReceived, now.timestamp, @@ -81,11 +81,11 @@ export class TrackStatsReporter { packetsNow = 0; } - const packetsBefore = StatsValueFormatter.getNonNegativeValue(before[key]); + const packetsBefore = ValueFormatter.getNonNegativeValue(before[key]); const packetsDiff = Math.max(0, packetsNow - packetsBefore); - const packetsLostNow = StatsValueFormatter.getNonNegativeValue(now.packetsLost); - const packetsLostBefore = StatsValueFormatter.getNonNegativeValue(before.packetsLost); + const packetsLostNow = ValueFormatter.getNonNegativeValue(now.packetsLost); + const packetsLostBefore = ValueFormatter.getNonNegativeValue(before.packetsLost); const packetsLostDiff = Math.max(0, packetsLostNow - packetsLostBefore); trackStats.setLoss({ @@ -101,8 +101,8 @@ export class TrackStatsReporter { nowTimestamp: number, beforeTimestamp: number, ): number { - const bytesNow = StatsValueFormatter.getNonNegativeValue(bytesNowAny); - const bytesBefore = StatsValueFormatter.getNonNegativeValue(bytesBeforeAny); + const bytesNow = ValueFormatter.getNonNegativeValue(bytesNowAny); + const bytesBefore = ValueFormatter.getNonNegativeValue(bytesBeforeAny); const bytesProcessed = Math.max(0, bytesNow - bytesBefore); const timeMs = nowTimestamp - beforeTimestamp; @@ -140,23 +140,44 @@ export class TrackStatsReporter { audioTrackSummary: TrackSummary; videoTrackSummary: TrackSummary; } { - const audioTrackSummary = { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }; - const videoTrackSummary = { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 }; - trackStatsList - .filter((t) => t.getType() === "remote") - .forEach((stats) => { - const trackSummary = stats.kind === "video" ? videoTrackSummary : audioTrackSummary; - trackSummary.count++; - if (stats.alive && stats.muted) { - trackSummary.muted++; - } - if (trackSummary.maxJitter < stats.getJitter()) { - trackSummary.maxJitter = stats.getJitter(); - } - if (trackSummary.maxPacketLoss < stats.getLoss().packetsLost) { - trackSummary.maxPacketLoss = stats.getLoss().packetsLost; - } - }); + const videoTrackSummary: TrackSummary = { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }; + const audioTrackSummary: TrackSummary = { + count: 0, + muted: 0, + maxJitter: 0, + maxPacketLoss: 0, + concealedAudio: 0, + totalAudio: 0, + }; + + const remoteTrackList = trackStatsList.filter((t) => t.getType() === "remote"); + const audioTrackList = remoteTrackList.filter((t) => t.kind === "audio"); + + remoteTrackList.forEach((stats) => { + const trackSummary = stats.kind === "video" ? videoTrackSummary : audioTrackSummary; + trackSummary.count++; + if (stats.alive && stats.muted) { + trackSummary.muted++; + } + if (trackSummary.maxJitter < stats.getJitter()) { + trackSummary.maxJitter = stats.getJitter(); + } + if (trackSummary.maxPacketLoss < stats.getLoss().packetsLost) { + trackSummary.maxPacketLoss = stats.getLoss().packetsLost; + } + if (audioTrackList.length > 0) { + trackSummary.concealedAudio += stats.getAudioConcealment()?.concealedAudio; + trackSummary.totalAudio += stats.getAudioConcealment()?.totalAudioDuration; + } + }); + return { audioTrackSummary, videoTrackSummary }; } @@ -167,10 +188,20 @@ export class TrackStatsReporter { const jitterStr = statsReport?.jitter; if (jitterStr !== undefined) { - const jitter = StatsValueFormatter.getNonNegativeValue(jitterStr); + const jitter = ValueFormatter.getNonNegativeValue(jitterStr); trackStats.setJitter(Math.round(jitter * 1000)); } else { trackStats.setJitter(-1); } } + + public static buildAudioConcealment(trackStats: MediaTrackStats, statsReport: any): void { + if (statsReport.type !== "inbound-rtp") { + return; + } + const msPerSample = (1000 * statsReport?.totalSamplesDuration) / statsReport?.totalSamplesReceived; + const concealedAudioDuration = msPerSample * statsReport?.concealedSamples; + const totalAudioDuration = 1000 * statsReport?.totalSamplesDuration; + trackStats.setAudioConcealment(concealedAudioDuration, totalAudioDuration); + } } diff --git a/src/webrtc/stats/transportStatsReporter.ts b/src/webrtc/stats/transportStatsBuilder.ts similarity index 98% rename from src/webrtc/stats/transportStatsReporter.ts rename to src/webrtc/stats/transportStatsBuilder.ts index d419a73972b..12ed7b0cbe4 100644 --- a/src/webrtc/stats/transportStatsReporter.ts +++ b/src/webrtc/stats/transportStatsBuilder.ts @@ -1,6 +1,6 @@ import { TransportStats } from "./transportStats"; -export class TransportStatsReporter { +export class TransportStatsBuilder { public static buildReport( report: RTCStatsReport | undefined, now: RTCIceCandidatePairStats, diff --git a/src/webrtc/stats/statsValueFormatter.ts b/src/webrtc/stats/valueFormatter.ts similarity index 96% rename from src/webrtc/stats/statsValueFormatter.ts rename to src/webrtc/stats/valueFormatter.ts index c658fa66504..bf75ce29f65 100644 --- a/src/webrtc/stats/statsValueFormatter.ts +++ b/src/webrtc/stats/valueFormatter.ts @@ -10,7 +10,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -export class StatsValueFormatter { +export class ValueFormatter { public static getNonNegativeValue(imput: any): number { let value = imput; diff --git a/tsconfig-build.json b/tsconfig-build.json index 3108314a4d6..5437c65b2db 100644 --- a/tsconfig-build.json +++ b/tsconfig-build.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "forceConsistentCasingInFileNames": true, "declarationMap": true, "sourceMap": true, "noEmit": false, diff --git a/typedoc.json b/typedoc.json index ca02e27c2a0..d0217d6625f 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,3 +1,9 @@ { - "plugin": ["typedoc-plugin-mdn-links", "typedoc-plugin-missing-exports", "typedoc-plugin-versions"] + "plugin": [ + "typedoc-plugin-mdn-links", + "typedoc-plugin-missing-exports", + "typedoc-plugin-versions", + "typedoc-plugin-coverage" + ], + "coverageLabel": "TypeDoc" } diff --git a/yarn.lock b/yarn.lock index a3d837ff1c1..8d013ba0dc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,9 +36,9 @@ "@jridgewell/trace-mapping" "^0.3.9" "@babel/cli@^7.12.10": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.21.0.tgz#1868eb70e9824b427fc607610cce8e9e7889e7e1" - integrity sha512-xi7CxyS8XjSyiwUGCfwf+brtJxjW1/ZTcBUkP10xawIEXLX5HzLn+3aXkgxozcP2UhRhtKTmQurw9Uaes7jZrA== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.21.5.tgz#a685a5b50b785f2edfbf6e042c1265c653547d9d" + integrity sha512-TOKytQ9uQW9c4np8F+P7ZfPINy5Kv+pizDIUwSVH8X5zHgYHV4AA8HE5LA450xXeu4jEfmUckTYvv1I4S26M/g== dependencies: "@jridgewell/trace-mapping" "^0.3.17" commander "^4.0.1" @@ -58,26 +58,26 @@ dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.4.tgz#457ffe647c480dff59c2be092fc3acf71195c87f" - integrity sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g== +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.5": + version "7.21.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.7.tgz#61caffb60776e49a57ba61a88f02bedd8714f6bc" + integrity sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA== "@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.5": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.4.tgz#c6dc73242507b8e2a27fd13a9c1814f9fa34a659" - integrity sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA== + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.8.tgz#2a8c7f0f53d60100ba4c32470ba0281c92aa9aa4" + integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ== dependencies: "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-compilation-targets" "^7.21.4" - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helpers" "^7.21.0" - "@babel/parser" "^7.21.4" + "@babel/generator" "^7.21.5" + "@babel/helper-compilation-targets" "^7.21.5" + "@babel/helper-module-transforms" "^7.21.5" + "@babel/helpers" "^7.21.5" + "@babel/parser" "^7.21.8" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.4" - "@babel/types" "^7.21.4" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -85,9 +85,9 @@ semver "^6.3.0" "@babel/eslint-parser@^7.12.10": - version "7.21.3" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.21.3.tgz#d79e822050f2de65d7f368a076846e7184234af7" - integrity sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg== + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.21.8.tgz#59fb6fc4f3b017ab86987c076226ceef7b2b2ef2" + integrity sha512-HLhI+2q+BP3sf78mFUZNCGc10KEmoUqtUT1OCdMZsN+qr4qFeLUod62/zAnF3jNQstwyasDkZnVXwfK2Bml7MQ== dependencies: "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" eslint-visitor-keys "^2.1.0" @@ -100,12 +100,12 @@ dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.12.11", "@babel/generator@^7.21.4", "@babel/generator@^7.7.2": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.4.tgz#64a94b7448989f421f919d5239ef553b37bb26bc" - integrity sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA== +"@babel/generator@^7.12.11", "@babel/generator@^7.21.5", "@babel/generator@^7.7.2": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.5.tgz#c0c0e5449504c7b7de8236d99338c3e2a340745f" + integrity sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w== dependencies: - "@babel/types" "^7.21.4" + "@babel/types" "^7.21.5" "@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" @@ -118,45 +118,46 @@ "@babel/types" "^7.18.6" "@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" - integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.21.5.tgz#817f73b6c59726ab39f6ba18c234268a519e5abb" + integrity sha512-uNrjKztPLkUk7bpCNC0jEKDJzzkvel/W+HguzbN8krA+LPfC1CEobJEvAvGka2A/M+ViOqXdcRL0GqPUJSjx9g== dependencies: - "@babel/helper-explode-assignable-expression" "^7.18.6" - "@babel/types" "^7.18.9" + "@babel/types" "^7.21.5" -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz#770cd1ce0889097ceacb99418ee6934ef0572656" - integrity sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg== +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz#631e6cc784c7b660417421349aac304c94115366" + integrity sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w== dependencies: - "@babel/compat-data" "^7.21.4" + "@babel/compat-data" "^7.21.5" "@babel/helper-validator-option" "^7.21.0" browserslist "^4.21.3" lru-cache "^5.1.1" semver "^6.3.0" "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz#3a017163dc3c2ba7deb9a7950849a9586ea24c18" - integrity sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q== + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.8.tgz#205b26330258625ef8869672ebca1e0dee5a0f02" + integrity sha512-+THiN8MqiH2AczyuZrnrKL6cAxFRRQDKW9h1YkBvbgKmAm6mwiacig1qT73DHIWMGo40GRnsEfN3LA+E6NtmSw== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-environment-visitor" "^7.21.5" "@babel/helper-function-name" "^7.21.0" - "@babel/helper-member-expression-to-functions" "^7.21.0" + "@babel/helper-member-expression-to-functions" "^7.21.5" "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.20.7" + "@babel/helper-replace-supers" "^7.21.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" "@babel/helper-split-export-declaration" "^7.18.6" + semver "^6.3.0" "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.4.tgz#40411a8ab134258ad2cf3a3d987ec6aa0723cee5" - integrity sha512-M00OuhU+0GyZ5iBBN9czjugzWrEq2vDpf/zCYHxxf93ul/Q5rv+a5h+/+0WnI1AebHNVtl5bFV0qsJoH23DbfA== + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.8.tgz#a7886f61c2e29e21fd4aaeaf1e473deba6b571dc" + integrity sha512-zGuSdedkFtsFHGbexAvNuipg1hbtitDLo2XE8/uf6Y9sOQV1xsYX/2pNbtedp/X0eU1pIt+kGvaqHCowkRbS5g== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" regexpu-core "^5.3.1" + semver "^6.3.0" "@babel/helper-define-polyfill-provider@^0.3.3": version "0.3.3" @@ -170,17 +171,10 @@ resolve "^1.14.2" semver "^6.1.2" -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-explode-assignable-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" - integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== - dependencies: - "@babel/types" "^7.18.6" +"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz#c769afefd41d171836f7cb63e295bedf689d48ba" + integrity sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ== "@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0": version "7.21.0" @@ -197,12 +191,12 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5" - integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q== +"@babel/helper-member-expression-to-functions@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.5.tgz#3b1a009af932e586af77c1030fba9ee0bde396c0" + integrity sha512-nIcGfgwpH2u4n9GG1HpStW5Ogx7x7ekiFHbjjFRKXbn5zUvqO9ZgotCO4x1aNbKn/x/xOUaXEhyNHCwtFCpxWg== dependencies: - "@babel/types" "^7.21.0" + "@babel/types" "^7.21.5" "@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.21.4": version "7.21.4" @@ -211,19 +205,19 @@ dependencies: "@babel/types" "^7.21.4" -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.2": - version "7.21.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2" - integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ== +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz#d937c82e9af68d31ab49039136a222b17ac0b420" + integrity sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw== dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-environment-visitor" "^7.21.5" + "@babel/helper-module-imports" "^7.21.4" + "@babel/helper-simple-access" "^7.21.5" "@babel/helper-split-export-declaration" "^7.18.6" "@babel/helper-validator-identifier" "^7.19.1" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.2" - "@babel/types" "^7.21.2" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" @@ -232,10 +226,10 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.21.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56" + integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg== "@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" @@ -247,24 +241,24 @@ "@babel/helper-wrap-function" "^7.18.9" "@babel/types" "^7.18.9" -"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331" - integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== +"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7", "@babel/helper-replace-supers@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.21.5.tgz#a6ad005ba1c7d9bc2973dfde05a1bba7065dde3c" + integrity sha512-/y7vBgsr9Idu4M6MprbOVUfH3vs7tsIfnVWv/Ml2xgwvyH6LTngdfbf5AdsKwkJy4zgy1X/kuNrEKvhhK28Yrg== dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.20.7" + "@babel/helper-environment-visitor" "^7.21.5" + "@babel/helper-member-expression-to-functions" "^7.21.5" "@babel/helper-optimise-call-expression" "^7.18.6" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== +"@babel/helper-simple-access@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz#d697a7971a5c39eac32c7e63c0921c06c8a249ee" + integrity sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg== dependencies: - "@babel/types" "^7.20.2" + "@babel/types" "^7.21.5" "@babel/helper-skip-transparent-expression-wrappers@^7.20.0": version "7.20.0" @@ -280,10 +274,10 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd" + integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" @@ -305,14 +299,14 @@ "@babel/traverse" "^7.20.5" "@babel/types" "^7.20.5" -"@babel/helpers@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.0.tgz#9dd184fb5599862037917cdc9eecb84577dc4e7e" - integrity sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA== +"@babel/helpers@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.5.tgz#5bac66e084d7a4d2d9696bdf0175a93f7fb63c08" + integrity sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA== dependencies: "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.0" - "@babel/types" "^7.21.0" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" "@babel/highlight@^7.18.6": version "7.18.6" @@ -323,10 +317,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.2.3", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" - integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.2.3", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8": + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" + integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -522,7 +516,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.19.0" -"@babel/plugin-syntax-import-meta@^7.8.3": +"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== @@ -606,12 +600,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-transform-arrow-functions@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551" - integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ== +"@babel/plugin-transform-arrow-functions@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.21.5.tgz#9bb42a53de447936a57ba256fbf537fc312b6929" + integrity sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/plugin-transform-async-to-generator@^7.20.7": version "7.20.7" @@ -651,12 +645,12 @@ "@babel/helper-split-export-declaration" "^7.18.6" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz#704cc2fd155d1c996551db8276d55b9d46e4d0aa" - integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ== +"@babel/plugin-transform-computed-properties@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.21.5.tgz#3a2d8bb771cd2ef1cd736435f6552fe502e11b44" + integrity sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/template" "^7.20.7" "@babel/plugin-transform-destructuring@^7.21.3": @@ -689,12 +683,12 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-for-of@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz#964108c9988de1a60b4be2354a7d7e245f36e86e" - integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ== +"@babel/plugin-transform-for-of@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz#e890032b535f5a2e237a18535f56a9fdaa7b83fc" + integrity sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/plugin-transform-function-name@^7.18.9": version "7.18.9" @@ -727,14 +721,14 @@ "@babel/helper-module-transforms" "^7.20.11" "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-transform-modules-commonjs@^7.21.2": - version "7.21.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz#6ff5070e71e3192ef2b7e39820a06fb78e3058e7" - integrity sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA== +"@babel/plugin-transform-modules-commonjs@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.5.tgz#d69fb947eed51af91de82e4708f676864e5e47bc" + integrity sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ== dependencies: - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-module-transforms" "^7.21.5" + "@babel/helper-plugin-utils" "^7.21.5" + "@babel/helper-simple-access" "^7.21.5" "@babel/plugin-transform-modules-systemjs@^7.20.11": version "7.20.11" @@ -791,12 +785,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-regenerator@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d" - integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== +"@babel/plugin-transform-regenerator@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz#576c62f9923f94bcb1c855adc53561fd7913724e" + integrity sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" regenerator-transform "^0.15.1" "@babel/plugin-transform-reserved-words@^7.18.6": @@ -864,12 +858,12 @@ "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-typescript" "^7.20.0" -"@babel/plugin-transform-unicode-escapes@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" - integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== +"@babel/plugin-transform-unicode-escapes@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.21.5.tgz#1e55ed6195259b0e9061d81f5ef45a9b009fb7f2" + integrity sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/plugin-transform-unicode-regex@^7.18.6": version "7.18.6" @@ -880,13 +874,13 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/preset-env@^7.12.11": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.21.4.tgz#a952482e634a8dd8271a3fe5459a16eb10739c58" - integrity sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.21.5.tgz#db2089d99efd2297716f018aeead815ac3decffb" + integrity sha512-wH00QnTTldTbf/IefEVyChtRdw5RJvODT/Vb4Vcxq1AZvtXj6T0YeX0cAcXhI6/BdGuiP3GcNIL4OQbI2DVNxg== dependencies: - "@babel/compat-data" "^7.21.4" - "@babel/helper-compilation-targets" "^7.21.4" - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/compat-data" "^7.21.5" + "@babel/helper-compilation-targets" "^7.21.5" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/helper-validator-option" "^7.21.0" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.20.7" @@ -911,6 +905,7 @@ "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-syntax-import-assertions" "^7.20.0" + "@babel/plugin-syntax-import-meta" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" @@ -920,22 +915,22 @@ "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.20.7" + "@babel/plugin-transform-arrow-functions" "^7.21.5" "@babel/plugin-transform-async-to-generator" "^7.20.7" "@babel/plugin-transform-block-scoped-functions" "^7.18.6" "@babel/plugin-transform-block-scoping" "^7.21.0" "@babel/plugin-transform-classes" "^7.21.0" - "@babel/plugin-transform-computed-properties" "^7.20.7" + "@babel/plugin-transform-computed-properties" "^7.21.5" "@babel/plugin-transform-destructuring" "^7.21.3" "@babel/plugin-transform-dotall-regex" "^7.18.6" "@babel/plugin-transform-duplicate-keys" "^7.18.9" "@babel/plugin-transform-exponentiation-operator" "^7.18.6" - "@babel/plugin-transform-for-of" "^7.21.0" + "@babel/plugin-transform-for-of" "^7.21.5" "@babel/plugin-transform-function-name" "^7.18.9" "@babel/plugin-transform-literals" "^7.18.9" "@babel/plugin-transform-member-expression-literals" "^7.18.6" "@babel/plugin-transform-modules-amd" "^7.20.11" - "@babel/plugin-transform-modules-commonjs" "^7.21.2" + "@babel/plugin-transform-modules-commonjs" "^7.21.5" "@babel/plugin-transform-modules-systemjs" "^7.20.11" "@babel/plugin-transform-modules-umd" "^7.18.6" "@babel/plugin-transform-named-capturing-groups-regex" "^7.20.5" @@ -943,17 +938,17 @@ "@babel/plugin-transform-object-super" "^7.18.6" "@babel/plugin-transform-parameters" "^7.21.3" "@babel/plugin-transform-property-literals" "^7.18.6" - "@babel/plugin-transform-regenerator" "^7.20.5" + "@babel/plugin-transform-regenerator" "^7.21.5" "@babel/plugin-transform-reserved-words" "^7.18.6" "@babel/plugin-transform-shorthand-properties" "^7.18.6" "@babel/plugin-transform-spread" "^7.20.7" "@babel/plugin-transform-sticky-regex" "^7.18.6" "@babel/plugin-transform-template-literals" "^7.18.9" "@babel/plugin-transform-typeof-symbol" "^7.18.9" - "@babel/plugin-transform-unicode-escapes" "^7.18.10" + "@babel/plugin-transform-unicode-escapes" "^7.21.5" "@babel/plugin-transform-unicode-regex" "^7.18.6" "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.21.4" + "@babel/types" "^7.21.5" babel-plugin-polyfill-corejs2 "^0.3.3" babel-plugin-polyfill-corejs3 "^0.6.0" babel-plugin-polyfill-regenerator "^0.4.1" @@ -972,14 +967,14 @@ esutils "^2.0.2" "@babel/preset-typescript@^7.12.7": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.4.tgz#b913ac8e6aa8932e47c21b01b4368d8aa239a529" - integrity sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.5.tgz#68292c884b0e26070b4d66b202072d391358395f" + integrity sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/helper-validator-option" "^7.21.0" "@babel/plugin-syntax-jsx" "^7.21.4" - "@babel/plugin-transform-modules-commonjs" "^7.21.2" + "@babel/plugin-transform-modules-commonjs" "^7.21.5" "@babel/plugin-transform-typescript" "^7.21.3" "@babel/register@^7.12.10": @@ -999,9 +994,9 @@ integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" - integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" + integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== dependencies: regenerator-runtime "^0.13.11" @@ -1014,28 +1009,28 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/traverse@^7.1.6", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4", "@babel/traverse@^7.7.2": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.4.tgz#a836aca7b116634e97a6ed99976236b3282c9d36" - integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q== +"@babel/traverse@^7.1.6", "@babel/traverse@^7.20.5", "@babel/traverse@^7.21.5", "@babel/traverse@^7.7.2": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133" + integrity sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw== dependencies: "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-environment-visitor" "^7.18.9" + "@babel/generator" "^7.21.5" + "@babel/helper-environment-visitor" "^7.21.5" "@babel/helper-function-name" "^7.21.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.21.4" - "@babel/types" "^7.21.4" + "@babel/parser" "^7.21.5" + "@babel/types" "^7.21.5" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.2.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4" - integrity sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA== +"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.2.0", "@babel/types@^7.20.0", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" + integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q== dependencies: - "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-string-parser" "^7.21.5" "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" @@ -1060,16 +1055,16 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@es-joy/jsdoccomment@~0.37.1": - version "0.37.1" - resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.37.1.tgz#fa32a41ba12097452693343e09ad4d26d157aedd" - integrity sha512-5vxWJ1gEkEF0yRd0O+uK6dHJf7adrxwQSX8PuRiPfFSAbNLnY0ZJfXaZucoz14Jj2N11xn2DnlEPwWRpYpvRjg== +"@es-joy/jsdoccomment@~0.39.3": + version "0.39.4" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.39.4.tgz#6b8a62e9b3077027837728818d3c4389a898b392" + integrity sha512-Jvw915fjqQct445+yron7Dufix9A+m9j1fCJYlCo1FWlRvTxa3pjJelxdSTdaLWcTwRU6vbL+NYjO4YuNIS5Qg== dependencies: comment-parser "1.3.1" esquery "^1.5.0" jsdoc-type-pratt-parser "~4.0.0" -"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== @@ -1077,18 +1072,18 @@ eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.4.0": - version "4.5.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.0.tgz#f6f729b02feee2c749f57e334b7a1b5f40a81724" - integrity sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ== + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" + integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== -"@eslint/eslintrc@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.2.tgz#01575e38707add677cf73ca1589abba8da899a02" - integrity sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ== +"@eslint/eslintrc@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" + integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.5.1" + espree "^9.5.2" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -1096,10 +1091,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.39.0": - version "8.39.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" - integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng== +"@eslint/js@8.40.0": + version "8.40.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.40.0.tgz#3ba73359e11f5a7bd3e407f70b3528abfae69cec" + integrity sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA== "@humanwhocodes/config-array@^0.11.8": version "0.11.8" @@ -1120,6 +1115,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -1419,13 +1426,14 @@ dependencies: lodash "^4.17.21" -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.7": - version "0.1.0-alpha.7" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.7.tgz#136375b84fd8a7e698f70fc969f668e541a61313" - integrity sha512-sQEG9cSfNji5NYBf5h7j5IxYVO0dwtAKoetaVyR+LhIXz/Su7zyEE3EwlAWAeJOFdAV/vZ5LTNyh39xADuNlTg== +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.9": + version "0.1.0-alpha.9" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.9.tgz#00bc266781502641a661858a5a521dd4d95275fc" + integrity sha512-g5cjpFwA9h0CbEGoAqNVI2QcyDsbI8FHoLo9+OXWHIezEKITsSv78mc5ilIwN+2YpmVlH0KNeQWTHw4vi0BMnw== "@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" + uid acd96c00a881d0f462e1f97a56c73742c8dbc984 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": @@ -1577,17 +1585,22 @@ dependencies: "@octokit/openapi-types" "^12.11.0" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@pkgr/utils@^2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.3.1.tgz#0a9b06ffddee364d6642b3cd562ca76f55b34a03" - integrity sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw== + version "2.4.0" + resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.4.0.tgz#b6373d2504aedaf2fc7cdf2d13ab1f48fa5f12d5" + integrity sha512-2OCURAmRtdlL8iUDTypMrrxfwe8frXTeXaxGsVOaYtc/wrUyk8Z/0OBetM7cdlsy7ZFWlMX72VogKeh+A4Xcjw== dependencies: cross-spawn "^7.0.3" + fast-glob "^3.2.12" is-glob "^4.0.3" - open "^8.4.0" + open "^9.1.0" picocolors "^1.0.0" - tiny-glob "^0.2.9" - tslib "^2.4.0" + tslib "^2.5.0" "@sinclair/typebox@^0.24.1": version "0.24.51" @@ -1599,19 +1612,19 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== -"@sinonjs/commons@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" - integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== +"@sinonjs/commons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== dependencies: type-detect "4.0.8" "@sinonjs/fake-timers@^10.0.2": - version "10.0.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" - integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== + version "10.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.1.0.tgz#3595e42b3f0a7df80a9681cf58d8cb418eac1e99" + integrity sha512-w1qd368vtrwttm1PRJWPW1QHlbmHrVDGs1eBH/jZvRPUFS4MNXV9Q33EQdjOdeAxZ7O8+3wM7zxztm2nfUSyKw== dependencies: - "@sinonjs/commons" "^2.0.0" + "@sinonjs/commons" "^3.0.0" "@tootallnate/once@2": version "2.0.0" @@ -1634,9 +1647,9 @@ integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== "@tsconfig/node16@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" - integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== "@types/babel-types@*", "@types/babel-types@^7.0.0": version "7.0.11" @@ -1670,9 +1683,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.18.3" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d" - integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== + version "7.18.5" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.5.tgz#c107216842905afafd3b6e774f6f935da6f5db80" + integrity sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q== dependencies: "@babel/types" "^7.3.0" @@ -1741,9 +1754,9 @@ "@types/istanbul-lib-report" "*" "@types/jest@^29.0.0": - version "29.5.0" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.0.tgz#337b90bbcfe42158f39c2fb5619ad044bbb518ac" - integrity sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg== + version "29.5.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.1.tgz#83c818aa9a87da27d6da85d3378e5a34d2f31a47" + integrity sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -1772,10 +1785,15 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node@*", "@types/node@18": - version "18.15.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" - integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== +"@types/node@*": + version "20.1.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.5.tgz#e94b604c67fc408f215fcbf3bd84d4743bf7f710" + integrity sha512-IvGD1CD/nego63ySR7vrAKEX3AJTcmrAN2kn+/sDNLi1Ff5kBzDeEdqWDplK+0HAEoLYej137Sk0cUU8OLOlMg== + +"@types/node@18": + version "18.16.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.10.tgz#9b16d918f4f6fec6cae4af34283a91d555b81519" + integrity sha512-sMo3EngB6QkMBlB9rBe1lFdKSLqljyWPPWv6/FzSxh/IDlyVWSzE9RiF4eAuerQHybrWdqBgAGb03PM89qOasA== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -1798,9 +1816,9 @@ integrity sha512-k3Nw6iaLoJbTjjY1tFT4L4IHNnLGJ52YacJNlNi6yqo76EYN1DiSPtuzzB7XnorZgrreUzCvzHDLklopmFdm7g== "@types/semver@^7.3.12": - version "7.3.13" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" - integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== + version "7.5.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" + integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== "@types/stack-utils@^2.0.0": version "2.0.1" @@ -1835,14 +1853,14 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.45.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.0.tgz#c0e10eeb936debe5d1c3433cf36206a95befefd0" - integrity sha512-p0QgrEyrxAWBecR56gyn3wkG15TJdI//eetInP3zYRewDh0XS+DhB3VUAd3QqvziFsfaQIoIuZMxZRB7vXYaYw== + version "5.59.6" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.6.tgz#a350faef1baa1e961698240f922d8de1761a9e2b" + integrity sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.0" - "@typescript-eslint/type-utils" "5.59.0" - "@typescript-eslint/utils" "5.59.0" + "@typescript-eslint/scope-manager" "5.59.6" + "@typescript-eslint/type-utils" "5.59.6" + "@typescript-eslint/utils" "5.59.6" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -1851,119 +1869,71 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.45.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.0.tgz#0ad7cd019346cc5d150363f64869eca10ca9977c" - integrity sha512-qK9TZ70eJtjojSUMrrEwA9ZDQ4N0e/AuoOIgXuNBorXYcBDk397D2r5MIe1B3cok/oCtdNC5j+lUUpVB+Dpb+w== + version "5.59.6" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.6.tgz#bd36f71f5a529f828e20b627078d3ed6738dbb40" + integrity sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA== dependencies: - "@typescript-eslint/scope-manager" "5.59.0" - "@typescript-eslint/types" "5.59.0" - "@typescript-eslint/typescript-estree" "5.59.0" + "@typescript-eslint/scope-manager" "5.59.6" + "@typescript-eslint/types" "5.59.6" + "@typescript-eslint/typescript-estree" "5.59.6" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.57.1": - version "5.57.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.57.1.tgz#5d28799c0fc8b501a29ba1749d827800ef22d710" - integrity sha512-N/RrBwEUKMIYxSKl0oDK5sFVHd6VI7p9K5MyUlVYAY6dyNb/wHUqndkTd3XhpGlXgnQsBkRZuu4f9kAHghvgPw== - dependencies: - "@typescript-eslint/types" "5.57.1" - "@typescript-eslint/visitor-keys" "5.57.1" - -"@typescript-eslint/scope-manager@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.0.tgz#86501d7a17885710b6716a23be2e93fc54a4fe8c" - integrity sha512-tsoldKaMh7izN6BvkK6zRMINj4Z2d6gGhO2UsI8zGZY3XhLq1DndP3Ycjhi1JwdwPRwtLMW4EFPgpuKhbCGOvQ== +"@typescript-eslint/scope-manager@5.59.6": + version "5.59.6" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.6.tgz#d43a3687aa4433868527cfe797eb267c6be35f19" + integrity sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ== dependencies: - "@typescript-eslint/types" "5.59.0" - "@typescript-eslint/visitor-keys" "5.59.0" + "@typescript-eslint/types" "5.59.6" + "@typescript-eslint/visitor-keys" "5.59.6" -"@typescript-eslint/type-utils@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.0.tgz#8e8d1420fc2265989fa3a0d897bde37f3851e8c9" - integrity sha512-d/B6VSWnZwu70kcKQSCqjcXpVH+7ABKH8P1KNn4K7j5PXXuycZTPXF44Nui0TEm6rbWGi8kc78xRgOC4n7xFgA== +"@typescript-eslint/type-utils@5.59.6": + version "5.59.6" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.6.tgz#37c51d2ae36127d8b81f32a0a4d2efae19277c48" + integrity sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ== dependencies: - "@typescript-eslint/typescript-estree" "5.59.0" - "@typescript-eslint/utils" "5.59.0" + "@typescript-eslint/typescript-estree" "5.59.6" + "@typescript-eslint/utils" "5.59.6" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.57.1": - version "5.57.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.57.1.tgz#d9989c7a9025897ea6f0550b7036027f69e8a603" - integrity sha512-bSs4LOgyV3bJ08F5RDqO2KXqg3WAdwHCu06zOqcQ6vqbTJizyBhuh1o1ImC69X4bV2g1OJxbH71PJqiO7Y1RuA== - -"@typescript-eslint/types@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32" - integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA== +"@typescript-eslint/types@5.59.6": + version "5.59.6" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.6.tgz#5a6557a772af044afe890d77c6a07e8c23c2460b" + integrity sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA== -"@typescript-eslint/typescript-estree@5.57.1": - version "5.57.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.57.1.tgz#10d9643e503afc1ca4f5553d9bbe672ea4050b71" - integrity sha512-A2MZqD8gNT0qHKbk2wRspg7cHbCDCk2tcqt6ScCFLr5Ru8cn+TCfM786DjPhqwseiS+PrYwcXht5ztpEQ6TFTw== +"@typescript-eslint/typescript-estree@5.59.6": + version "5.59.6" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz#2fb80522687bd3825504925ea7e1b8de7bb6251b" + integrity sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA== dependencies: - "@typescript-eslint/types" "5.57.1" - "@typescript-eslint/visitor-keys" "5.57.1" + "@typescript-eslint/types" "5.59.6" + "@typescript-eslint/visitor-keys" "5.59.6" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.0.tgz#8869156ee1dcfc5a95be3ed0e2809969ea28e965" - integrity sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg== - dependencies: - "@typescript-eslint/types" "5.59.0" - "@typescript-eslint/visitor-keys" "5.59.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.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.0.tgz#063d066b3bc4850c18872649ed0da9ee72d833d5" - integrity sha512-GGLFd+86drlHSvPgN/el6dRQNYYGOvRSDVydsUaQluwIW3HvbXuxyuD5JETvBt/9qGYe+lOrDk6gRrWOHb/FvA== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.0" - "@typescript-eslint/types" "5.59.0" - "@typescript-eslint/typescript-estree" "5.59.0" - eslint-scope "^5.1.1" - semver "^7.3.7" - -"@typescript-eslint/utils@^5.10.0": - version "5.57.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.57.1.tgz#0f97b0bbd88c2d5e2036869f26466be5f4c69475" - integrity sha512-kN6vzzf9NkEtawECqze6v99LtmDiUJCVpvieTFA1uL7/jDghiJGubGZ5csicYHU1Xoqb3oH/R5cN5df6W41Nfg== +"@typescript-eslint/utils@5.59.6", "@typescript-eslint/utils@^5.10.0": + version "5.59.6" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.6.tgz#82960fe23788113fc3b1f9d4663d6773b7907839" + integrity sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.57.1" - "@typescript-eslint/types" "5.57.1" - "@typescript-eslint/typescript-estree" "5.57.1" + "@typescript-eslint/scope-manager" "5.59.6" + "@typescript-eslint/types" "5.59.6" + "@typescript-eslint/typescript-estree" "5.59.6" eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.57.1": - version "5.57.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.57.1.tgz#585e5fa42a9bbcd9065f334fd7c8a4ddfa7d905e" - integrity sha512-RjQrAniDU0CEk5r7iphkm731zKlFiUjvcBS2yHAg8WWqFMCaCrD0rKEVOMUyMMcbGPZ0bPp56srkGWrgfZqLRA== - dependencies: - "@typescript-eslint/types" "5.57.1" - eslint-visitor-keys "^3.3.0" - -"@typescript-eslint/visitor-keys@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.0.tgz#a59913f2bf0baeb61b5cfcb6135d3926c3854365" - integrity sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA== +"@typescript-eslint/visitor-keys@5.59.6": + version "5.59.6" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz#673fccabf28943847d0c8e9e8d008e3ada7be6bb" + integrity sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q== dependencies: - "@typescript-eslint/types" "5.59.0" + "@typescript-eslint/types" "5.59.6" eslint-visitor-keys "^3.3.0" JSONStream@^1.0.3: @@ -1980,9 +1950,9 @@ abab@^2.0.6: integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== ace-builds@^1.4.13: - version "1.16.0" - resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.16.0.tgz#b4d38f9566157510fc22ee183d4f96b978f4fcc8" - integrity sha512-EriMhoxdfhh0zKm7icSt8EXekODAOVsYh9fpnlru9ALwf0Iw7J7bpuqLjhi3QRxvVKR7P0teQdJwTvjVMcYHuw== + version "1.21.1" + resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.21.1.tgz#fb73589114725795babac7cedbbb74cd438c715b" + integrity sha512-GHHtz76BnQc5PcAAjJK6tPbuktSKQ+Vu5MDlu2+hDS4NtiofrttGBJHaxcD4741WZwsAO7Dk1I8w2a4n204T+Q== acorn-globals@^3.0.0: version "3.1.0" @@ -2100,6 +2070,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-sequence-parser@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz#4d790f31236ac20366b23b3916b789e1bde39aed" @@ -2124,6 +2099,11 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + ansicolors@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" @@ -2243,6 +2223,11 @@ ast-types@^0.14.2: dependencies: tslib "^2.0.1" +async@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2411,6 +2396,11 @@ better-docs@^2.4.0-beta.9: vue-docgen-api "^3.26.0" vue2-ace-editor "^0.0.15" +big-integer@^1.6.44: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -2426,6 +2416,13 @@ bn.js@^5.0.0, bn.js@^5.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== +bplist-parser@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.2.0.tgz#43a9d183e5bf9d545200ceac3e712f79ebbe8d0e" + integrity sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw== + dependencies: + big-integer "^1.6.44" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2654,6 +2651,13 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== +bundle-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-3.0.0.tgz#ba59bcc9ac785fb67ccdbf104a2bf60c099f0e1a" + integrity sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw== + dependencies: + run-applescript "^5.0.0" + c8@^7.6.0: version "7.13.0" resolved "https://registry.yarnpkg.com/c8/-/c8-7.13.0.tgz#a2a70a851278709df5a9247d62d7f3d4bcb5f2e4" @@ -2706,9 +2710,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001449: - version "1.0.30001477" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001477.tgz#a2ffb2276258233034bbb869d4558b02658a511e" - integrity sha512-lZim4iUHhGcy5p+Ri/G7m84hJwncj+Kz7S5aD4hoQfslKZJgt0tHc/hafVbqHC5bbhHb+mrW2JOUHkI5KH7toQ== + version "1.0.30001487" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001487.tgz#d882d1a34d89c11aea53b8cdc791931bdab5fe1b" + integrity sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA== center-align@^0.1.1: version "0.1.3" @@ -2718,7 +2722,7 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2762,7 +2766,7 @@ chokidar@^3.4.0: optionalDependencies: fsevents "~2.3.2" -ci-info@^3.2.0, ci-info@^3.6.1: +ci-info@^3.2.0, ci-info@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== @@ -2805,6 +2809,14 @@ cli-color@^2.0.0: memoizee "^0.4.15" timers-ext "^0.1.7" +cli-diff@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cli-diff/-/cli-diff-1.0.0.tgz#c56f1fa17849a629bf07154a2bd199aefe742964" + integrity sha512-XOVrll4VMhxBv26WqV6OH9cWqRxBXthh3uZ3dtg+CLqB8m0R6QJiSoDIXQNXDAeo/FAkQ+kF9Ph8NhQskU3LpQ== + dependencies: + chalk "^2.4.1" + diff "^3.5.0" + cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -2968,9 +2980,9 @@ convert-source-map@~1.1.0: integrity sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg== core-js-compat@^3.25.1: - version "3.30.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.30.0.tgz#99aa2789f6ed2debfa1df3232784126ee97f4d80" - integrity sha512-P5A2h/9mRYZFIAP+5Ab8ns6083IyVpSclU74UNvbGVQ8VM7n3n3/g2yF3AkKQ9NXz2O+ioxLbEWKnDtgsFamhg== + version "3.30.2" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.30.2.tgz#83f136e375babdb8c80ad3c22d67c69098c1dd8b" + integrity sha512-nriW1nuJjUgvkEjIot1Spwakz52V9YkYHZAQG6A1eCgC8AA1p0zngrQEP9R0+V6hji5XilWKG1Bd0YRppmGimA== dependencies: browserslist "^4.21.5" @@ -2980,9 +2992,9 @@ core-js@^2.4.0: integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.0.0: - version "3.30.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.0.tgz#64ac6f83bc7a49fd42807327051701d4b1478dea" - integrity sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg== + version "3.30.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.2.tgz#6528abfda65e5ad728143ea23f7a14f0dcf503fc" + integrity sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg== core-util-is@~1.0.0: version "1.0.3" @@ -3134,12 +3146,30 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== +default-browser-id@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-3.0.0.tgz#bee7bbbef1f4e75d31f98f4d3f1556a14cea790c" + integrity sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA== + dependencies: + bplist-parser "^0.2.0" + untildify "^4.0.0" + +default-browser@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-4.0.0.tgz#53c9894f8810bf86696de117a6ce9085a3cbc7da" + integrity sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA== + dependencies: + bundle-name "^3.0.0" + default-browser-id "^3.0.0" + execa "^7.1.1" + titleize "^3.0.0" + +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== -define-properties@^1.1.3, define-properties@^1.1.4: +define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== @@ -3209,6 +3239,11 @@ diff-sequences@^29.4.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== +diff@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -3282,10 +3317,15 @@ duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: dependencies: readable-stream "^2.0.2" +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + electron-to-chromium@^1.4.284: - version "1.4.356" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.356.tgz#b75a8a8c31d571f6024310cc980a08cd6c15a8c5" - integrity sha512-nEftV1dRX3omlxAj42FwqRZT0i4xd2dIg39sog/CnCJeCcL1TRd2Uh0i9Oebgv8Ou0vzTPw++xc+Z20jzS2B6A== + version "1.4.396" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.396.tgz#3d3664eb58d86376fbe2fece3705f68ca197205c" + integrity sha512-pqKTdqp/c5vsrc0xUPYXTDBo9ixZuGY8es4ZOjjd6HD6bFYbu5QA09VoW3fkY4LF1T0zYk86lN6bZnNlBuOpdQ== elliptic@^6.5.3: version "6.5.4" @@ -3310,18 +3350,23 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + enhanced-resolve@^5.12.0: - version "5.12.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" - integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== + version "5.14.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.14.0.tgz#0b6c676c8a3266c99fa281e4433a706f5c0c61c4" + integrity sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" entities@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" - integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" @@ -3497,9 +3542,9 @@ eslint-import-resolver-typescript@^3.5.1: synckit "^0.8.5" eslint-module-utils@^2.7.4: - version "2.7.4" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" - integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" + integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== dependencies: debug "^3.2.7" @@ -3531,18 +3576,18 @@ eslint-plugin-jest@^27.1.6: dependencies: "@typescript-eslint/utils" "^5.10.0" -eslint-plugin-jsdoc@^43.0.6: - version "43.1.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-43.1.1.tgz#fc72ba21597cc99b1a0dc988aebb9bb57d0ec492" - integrity sha512-J2kjjsJ5vBXSyNzqJhceeSGTAgVgZHcPSJKo3vD4tNjUdfky98rR2VfZUDsS1GKL6isyVa8GWvr+Az7Vyg2HXA== +eslint-plugin-jsdoc@^44.0.0: + version "44.2.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-44.2.4.tgz#0bdc163771504ec7330414eda6a7dbae67156ddb" + integrity sha512-/EMMxCyRh1SywhCb66gAqoGX4Yv6Xzc4bsSkF1AiY2o2+bQmGMQ05QZ5+JjHbdFTPDZY9pfn+DsSNP0a5yQpIg== dependencies: - "@es-joy/jsdoccomment" "~0.37.1" + "@es-joy/jsdoccomment" "~0.39.3" are-docs-informative "^0.0.2" comment-parser "1.3.1" debug "^4.3.4" escape-string-regexp "^4.0.0" esquery "^1.5.0" - semver "^7.5.0" + semver "^7.5.1" spdx-expression-parse "^3.0.1" eslint-plugin-matrix-org@^1.0.0: @@ -3558,24 +3603,24 @@ eslint-plugin-tsdoc@^0.2.17: "@microsoft/tsdoc" "0.14.2" "@microsoft/tsdoc-config" "0.16.2" -eslint-plugin-unicorn@^46.0.0: - version "46.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-46.0.0.tgz#b5cdcc9465fd6e46ab7968b87dd4a43adc8d6031" - integrity sha512-j07WkC+PFZwk8J33LYp6JMoHa1lXc1u6R45pbSAipjpfpb7KIGr17VE2D685zCxR5VL4cjrl65kTJflziQWMDA== +eslint-plugin-unicorn@^47.0.0: + version "47.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-47.0.0.tgz#960e9d3789f656ba3e21982420793b069a911011" + integrity sha512-ivB3bKk7fDIeWOUmmMm9o3Ax9zbMz1Bsza/R2qm46ufw4T6VBFBaJIR1uN3pCKSmSXm8/9Nri8V+iUut1NhQGA== dependencies: "@babel/helper-validator-identifier" "^7.19.1" - "@eslint-community/eslint-utils" "^4.1.2" - ci-info "^3.6.1" + "@eslint-community/eslint-utils" "^4.4.0" + ci-info "^3.8.0" clean-regexp "^1.0.0" - esquery "^1.4.0" + esquery "^1.5.0" indent-string "^4.0.0" - is-builtin-module "^3.2.0" + is-builtin-module "^3.2.1" 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.10.0" safe-regex "^2.1.1" semver "^7.3.8" strip-indent "^3.0.0" @@ -3606,20 +3651,20 @@ eslint-visitor-keys@^2.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc" - integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ== +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" + integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@8.39.0: - version "8.39.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1" - integrity sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og== +eslint@8.40.0: + version "8.40.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.40.0.tgz#a564cd0099f38542c4e9a2f630fa45bf33bc42a4" + integrity sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.2" - "@eslint/js" "8.39.0" + "@eslint/eslintrc" "^2.0.3" + "@eslint/js" "8.40.0" "@humanwhocodes/config-array" "^0.11.8" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -3630,8 +3675,8 @@ eslint@8.39.0: doctrine "^3.0.0" escape-string-regexp "^4.0.0" eslint-scope "^7.2.0" - eslint-visitor-keys "^3.4.0" - espree "^9.5.1" + eslint-visitor-keys "^3.4.1" + espree "^9.5.2" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -3657,21 +3702,21 @@ eslint@8.39.0: strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.5.1: - version "9.5.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.1.tgz#4f26a4d5f18905bf4f2e0bd99002aab807e96dd4" - integrity sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg== +espree@^9.5.2: + version "9.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" + integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== dependencies: acorn "^8.8.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.0" + eslint-visitor-keys "^3.4.1" esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.0, esquery@^1.4.2, esquery@^1.5.0: +esquery@^1.4.2, esquery@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== @@ -3745,6 +3790,21 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +execa@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-7.1.1.tgz#3eb3c83d239488e7b409d48e8813b76bb55c9c43" + integrity sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.1" + human-signals "^4.3.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^3.0.7" + strip-final-newline "^3.0.0" + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -3801,7 +3861,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.11, fast-glob@^3.2.9: +fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== @@ -3943,6 +4003,14 @@ foreground-child@^2.0.0: cross-spawn "^7.0.0" signal-exit "^3.0.2" +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -3991,7 +4059,7 @@ function.prototype.name@^1.1.5: es-abstract "^1.19.0" functions-have-names "^1.2.2" -functions-have-names@^1.2.2: +functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== @@ -4012,12 +4080,13 @@ get-caller-file@^2.0.5: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== dependencies: function-bind "^1.1.1" has "^1.0.3" + has-proto "^1.0.1" has-symbols "^1.0.3" get-package-type@^0.1.0: @@ -4025,7 +4094,7 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stream@^6.0.0: +get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== @@ -4062,6 +4131,17 @@ glob-to-regexp@^0.4.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== +glob@^10.0.0: + version "10.2.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.4.tgz#f5bf7ddb080e3e9039b148a9e2aef3d5ebfc0a25" + integrity sha512-fDboBse/sl1oXSLhIp0FcCJgzW9KmhC/q8ULTKC82zc+DL3TL7FNb8qlt5qqXN53MsKEUSIcb+7DLmEygOE5Yw== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.0.3" + minimatch "^9.0.0" + minipass "^5.0.0 || ^6.0.0" + path-scurry "^1.7.0" + glob@^7.1.0, glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -4074,16 +4154,6 @@ glob@^7.1.0, glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^9.2.0: - version "9.3.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" - integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== - dependencies: - fs.realpath "^1.0.0" - minimatch "^8.0.2" - minipass "^4.2.4" - path-scurry "^1.6.1" - glob@~3.2.7: version "3.2.11" resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" @@ -4111,11 +4181,6 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" -globalyzer@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" - integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== - globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -4129,9 +4194,9 @@ globby@^11.1.0: slash "^3.0.0" globby@^13.1.3: - version "13.1.3" - resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.3.tgz#f62baf5720bcb2c1330c8d4ef222ee12318563ff" - integrity sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw== + version "13.1.4" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.4.tgz#2f91c116066bcec152465ba36e5caa4a13c01317" + integrity sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g== dependencies: dir-glob "^3.0.1" fast-glob "^3.2.11" @@ -4139,11 +4204,6 @@ globby@^13.1.3: merge2 "^1.4.1" slash "^4.0.0" -globrex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" - integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -4292,6 +4352,11 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +human-signals@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" + integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== + iconv-lite@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -4434,7 +4499,7 @@ is-buffer@^1.1.0, is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-builtin-module@^3.2.0: +is-builtin-module@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== @@ -4447,9 +4512,9 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== is-core-module@^2.1.0, is-core-module@^2.11.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== + version "2.12.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.0.tgz#36ad62f6f73c8253fd6472517a12483cf03e7ec4" + integrity sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ== dependencies: has "^1.0.3" @@ -4460,11 +4525,16 @@ is-date-object@^1.0.1: dependencies: has-tostringtag "^1.0.0" -is-docker@^2.0.0, is-docker@^2.1.1: +is-docker@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + is-expression@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-3.0.0.tgz#39acaa6be7fd1f3471dc42c7416e61c24317ac9f" @@ -4502,6 +4572,13 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" @@ -4566,6 +4643,11 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -4677,6 +4759,15 @@ istanbul-reports@^3.1.3, istanbul-reports@^3.1.4: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +jackspeak@^2.0.3: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.0.tgz#497cbaedc902ec3f31d5d61be804d2364ff9ddad" + integrity sha512-r5XBrqIJfwRIjRt/Xr5fv9Wh09qyhHfKnYddDlpM+ibRR20qrYActpCAgU6U+d53EOEjzkvxPMVHSlgR7leXrQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jest-changed-files@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" @@ -5407,10 +5498,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.0.0.tgz#daece36a9fc332e93f8e75f3fcfd17900253567c" - integrity sha512-9AEKXzvOZc4BMacFnYiTOlDH/197LNnQIK9wZ6iMB5NXPzuv4bWR/Msv7iUMplkiMQ1qQL+KSv/JF1mZAB5Lrg== +lru-cache@^9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.1.tgz#c58a93de58630b688de39ad04ef02ef26f1902f1" + integrity sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A== lru-queue@^0.1.0: version "0.1.0" @@ -5469,9 +5560,9 @@ matrix-mock-request@^2.5.0: expect "^28.1.0" matrix-widget-api@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5" - integrity sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q== + version "1.4.0" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.4.0.tgz#e426ec16a013897f3a4a9c2bff423f54ab0ba745" + integrity sha512-dw0dRylGQzDUoiaY/g5xx1tBbS7aoov31PRtFMAvG58/4uerYllV9Gfou7w+I1aglwB6hihTREzKltVjARWV6A== dependencies: "@types/events" "^3.0.0" events "^3.2.0" @@ -5542,6 +5633,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -5572,13 +5668,6 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatc dependencies: brace-expansion "^1.1.7" -minimatch@^8.0.2: - version "8.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" - integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== - dependencies: - brace-expansion "^2.0.1" - minimatch@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.0.tgz#bfc8e88a1c40ffd40c172ddac3decb8451503b56" @@ -5596,15 +5685,10 @@ minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -minipass@^4.2.4: - version "4.2.7" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.7.tgz#14c6fc0dcab54d9c4dd64b2b7032fef04efec218" - integrity sha512-ScVIgqHcXRMyfflqHmEW0bm8z8rb5McHyOY3ewX9JBgZaR77G7nxq9L/mtV96/QbAAwtbCAHVVLzD1kkyfFQEw== - -minipass@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" - integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== +"minipass@^5.0.0 || ^6.0.0": + version "6.0.1" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-6.0.1.tgz#315417c259cb32a1b2fc530c0e7f55c901a60a6d" + integrity sha512-Tenl5QPpgozlOGBiveNYHg2f6y+VpxsXRoIHFUVJuSmTonXRAE6q9b8Mp/O46762/2AlW4ye4Nkyvx0fgWDKbw== mkdirp-classic@^0.5.2: version "0.5.3" @@ -5683,9 +5767,9 @@ node-dir@^0.1.10: minimatch "^3.0.2" node-fetch@^2.6.7: - version "2.6.9" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" - integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + version "2.6.11" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" + integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== dependencies: whatwg-url "^5.0.0" @@ -5721,10 +5805,17 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npm-run-path@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00" + integrity sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q== + dependencies: + path-key "^4.0.0" + nwsapi@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.3.tgz#00e04dfd5a4a751e5ec2fecdc75dfd2f0db820fa" - integrity sha512-jscxIO4/VKScHlbmFBdV1Z6LXnLO+ZR4VMtypudUdfwtKxUN3TQcNFIHLwKtrUbDyHN4/GycY9+oRGZ2XMXYPw== + version "2.2.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.4.tgz#fd59d5e904e8e1f03c25a7d5a15cfa16c714a1e5" + integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g== object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" @@ -5779,13 +5870,21 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -open@^8.4.0: - version "8.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" - integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + +open@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/open/-/open-9.1.0.tgz#684934359c90ad25742f5a26151970ff8c6c80b6" + integrity sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg== dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" + default-browser "^4.0.0" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" is-wsl "^2.2.0" optionator@^0.8.1: @@ -5944,6 +6043,11 @@ 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-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + 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" @@ -5954,13 +6058,13 @@ path-platform@~0.11.15: resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" integrity sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg== -path-scurry@^1.6.1: - version "1.6.4" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.6.4.tgz#020a9449e5382a4acb684f9c7e1283bc5695de66" - integrity sha512-Qp/9IHkdNiXJ3/Kon++At2nVpnhRiPq/aSvQN+H3U1WZbvNRK0RIQK/o4HMqPoXjpuGJUEWpHSs6Mnjxqh3TQg== +path-scurry@^1.7.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.9.1.tgz#838566bb22e38feaf80ecd49ae06cd12acd782ee" + integrity sha512-UgmoiySyjFxP6tscZDgWGEAgsW5ok8W3F5CJDnnH2pozwSTGE6eH7vwTotMwATWA2r5xqdkKdxYPkwlJjAI/3g== dependencies: - lru-cache "^9.0.0" - minipass "^5.0.0" + lru-cache "^9.1.1" + minipass "^5.0.0 || ^6.0.0" path-to-regexp@^2.2.1: version "2.4.0" @@ -6078,7 +6182,7 @@ promise@^7.0.1: dependencies: asap "~2.0.3" -prompts@^2.0.1: +prompts@^2.0.1, prompts@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== @@ -6238,9 +6342,9 @@ punycode@^2.1.0, punycode@^2.1.1: integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== pure-rand@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.1.tgz#31207dddd15d43f299fdcdb2f572df65030c19af" - integrity sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg== + version "6.0.2" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306" + integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ== querystring-es3@~0.2.0: version "0.2.1" @@ -6438,18 +6542,18 @@ regenerator-transform@^0.15.1: "@babel/runtime" "^7.8.4" regexp-tree@^0.1.24, regexp-tree@~0.1.1: - version "0.1.24" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.24.tgz#3d6fa238450a4d66e5bc9c4c14bb720e2196829d" - integrity sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw== + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== regexp.prototype.flags@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" - integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + version "1.5.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" + integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - functions-have-names "^1.2.2" + define-properties "^1.2.0" + functions-have-names "^1.2.3" regexpu-core@^5.3.1: version "5.3.2" @@ -6463,6 +6567,13 @@ regexpu-core@^5.3.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" +regjsparser@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.10.0.tgz#b1ed26051736b436f22fdec1c8f72635f9f44892" + integrity sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA== + dependencies: + jsesc "~0.5.0" + regjsparser@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" @@ -6553,12 +6664,12 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -rimraf@^4.0.0: - version "4.4.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.1.tgz#bd33364f67021c5b79e93d7f4fa0568c7c21b755" - integrity sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og== +rimraf@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.0.tgz#5bda14e410d7e4dd522154891395802ce032c2cb" + integrity sha512-Jf9llaP+RvaEVS5nPShYFhtXIrb3LRKP281ib3So0KkeZKo2wIKyq0Re7TOSwanasA423PSr6CCIL4bP6T040g== dependencies: - glob "^9.2.0" + glob "^10.0.0" ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" @@ -6568,6 +6679,13 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +run-applescript@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-5.0.0.tgz#e11e1c932e055d5c6b40d98374e0268d9b11899c" + integrity sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg== + dependencies: + execa "^5.0.0" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -6633,17 +6751,10 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.5, semver@^7.3.8: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.7, semver@^7.5.0: - version "7.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" - integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== +semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1: + version "7.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" + integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== dependencies: lru-cache "^6.0.0" @@ -6687,9 +6798,9 @@ shell-quote@^1.6.1: integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== shiki@^0.14.1: - version "0.14.1" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.1.tgz#9fbe082d0a8aa2ad63df4fbf2ee11ec924aa7ee1" - integrity sha512-+Jz4nBkCBe0mEDqo1eKRcCdjRtrCjozmcbTUjbPTX7OOJfEbTZzlUWlZtGe3Gb5oV1/jnojhG//YZc3rs9zSEw== + version "0.14.2" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.2.tgz#d51440800b701392b31ce2336036058e338247a1" + integrity sha512-ltSZlSLOuSY0M0Y75KA+ieRaZ0Trf5Wl3gutE7jzLuIcWxLp5i/uEnLoQWNvgKXQ5OMpGkJnVMRLAuzjc0LJ2A== dependencies: ansi-sequence-parser "^1.1.0" jsonc-parser "^3.2.0" @@ -6715,6 +6826,11 @@ signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" + integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== + simple-concat@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" @@ -6846,7 +6962,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6855,6 +6971,15 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string.prototype.trim@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" @@ -6901,13 +7026,20 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" + integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -6930,6 +7062,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -7006,9 +7143,9 @@ tapable@^2.2.0: integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== terser@^5.5.1: - version "5.17.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.1.tgz#948f10830454761e2eeedc6debe45c532c83fd69" - integrity sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw== + version "5.17.4" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.4.tgz#b0c2d94897dfeba43213ed5f90ed117270a2c696" + integrity sha512-jcEKZw6UPrgugz/0Tuk/PVyLAPfMBJf5clnGueo45wTweoV8yh7Q7PEkhkJ5uuUbC7zAxEcG3tqNr1bstkQ8nw== dependencies: "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" @@ -7070,13 +7207,10 @@ timers-ext@^0.1.7: es5-ext "~0.10.46" next-tick "1" -tiny-glob@^0.2.9: - version "0.2.9" - resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" - integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== - dependencies: - globalyzer "0.1.0" - globrex "^0.1.2" +titleize@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53" + integrity sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ== tmpl@1.0.5: version "1.0.5" @@ -7202,7 +7336,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.1, tslib@^2.4.0, tslib@^2.5.0: +tslib@^2.0.1, tslib@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== @@ -7287,6 +7421,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== +typedoc-plugin-coverage@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/typedoc-plugin-coverage/-/typedoc-plugin-coverage-2.1.0.tgz#619bf10853c5851c47dc17585e14385647bbb754" + integrity sha512-d0Lc/aOPRAMnfABCW2cQqCQdzLUzadeq62r4DBrSchcfzx1X8nOvhXK/n4mVAO4wQQUchTm2ZGAzTtiAc2nl0A== + typedoc-plugin-mdn-links@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-3.0.3.tgz#da8d1a9750d57333e6c21717b38bfc13d4058de2" @@ -7297,6 +7436,17 @@ typedoc-plugin-missing-exports@^2.0.0: resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-2.0.0.tgz#9bdc4e30b0c7f24e9f1cb8890db4d01f608717c5" integrity sha512-t0QlKCm27/8DaheJkLo/gInSNjzBXgSciGhoLpL6sLyXZibm7SuwJtHvg4qXI2IjJfFBgW9mJvvszpoxMyB0TA== +typedoc-plugin-versions-cli@^0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/typedoc-plugin-versions-cli/-/typedoc-plugin-versions-cli-0.1.12.tgz#3e20c4e4078d8aec827dc3cc4686069d6e3c8ab2" + integrity sha512-tmGXo8T6gGW3hajMh+cZTRo50w6JJyOuCWBALGxZM0TOaRL4n0J3SanO2vFUrVd25QR/O5/1pdnTKW1ldcXmXg== + dependencies: + async "^3.2.4" + cli-diff "^1.0.0" + prompts "^2.4.2" + semver "^7.3.7" + yargs "^17.5.1" + typedoc-plugin-versions@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/typedoc-plugin-versions/-/typedoc-plugin-versions-0.2.3.tgz#2cae4d722e45c3d9ab1a7640349c970dfc880f86" @@ -7306,9 +7456,9 @@ typedoc-plugin-versions@^0.2.3: semver "^7.3.7" typedoc@^0.24.0: - version "0.24.6" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.24.6.tgz#1f76fac27db9c6626b5b5c79f78f3c813b285827" - integrity sha512-c3y3h45xJv3qYwKDAwU6Cl+26CjT0ZvblHzfHJ+SjQDM4p1mZxtgHky4lhmG0+nNarRht8kADfZlbspJWdZarQ== + version "0.24.7" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.24.7.tgz#7eeb272a1894b3789acc1a94b3f2ae8e7330ee39" + integrity sha512-zzfKDFIZADA+XRIp2rMzLe9xZ6pt12yQOhCr7cD7/PBTjhPmMyMvGrkZ2lPNJitg3Hj1SeiYFNzCsSDrlpxpKw== dependencies: lunr "^2.3.9" marked "^4.3.0" @@ -7433,10 +7583,15 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + update-browserslist-db@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -7713,7 +7868,7 @@ wordwrap@0.0.2: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" integrity sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q== -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -7722,6 +7877,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -7810,10 +7974,10 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.1, yargs@^17.3.1: - version "17.7.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967" - integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw== +yargs@^17.0.1, yargs@^17.3.1, yargs@^17.5.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" escalade "^3.1.1"