diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index df3a62ea757..8b1c7d475df 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -38,4 +38,5 @@ cee7f7a280a8c20bafc21c0a2911f60851f7a7ca 7ed65407e6cdf292ce3cf659310c68d19dcd52b2 # Switch to ESLint from JSHint (Google eslint rules as a base) e057956ede9ad1a931ff8050c411aca7907e0394 - +# prettier +349c2c2587c2885bb69eda4aa078b5383724cf5e diff --git a/CHANGELOG.md b/CHANGELOG.md index e05be659ebf..0ab66068d20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +Changes in [23.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0) (2023-01-18) +================================================================================================== + +## 🦖 Deprecations + * Remove extensible events v1 field population on legacy events ([\#3040](https://github.com/matrix-org/matrix-js-sdk/pull/3040)). + +## ✨ Features + * Improve hasUserReadEvent and getUserReadUpTo realibility with threads ([\#3031](https://github.com/matrix-org/matrix-js-sdk/pull/3031)). Fixes vector-im/element-web#24164. + * Remove video track when muting video ([\#3028](https://github.com/matrix-org/matrix-js-sdk/pull/3028)). Fixes vector-im/element-call#209. + * Make poll start event type available (PSG-962) ([\#3034](https://github.com/matrix-org/matrix-js-sdk/pull/3034)). + * Add alt event type matching in Relations model ([\#3018](https://github.com/matrix-org/matrix-js-sdk/pull/3018)). + * Remove usage of v1 Identity Server API ([\#3003](https://github.com/matrix-org/matrix-js-sdk/pull/3003)). + * Add `device_id` to `/account/whoami` types ([\#3005](https://github.com/matrix-org/matrix-js-sdk/pull/3005)). + * Implement MSC3912: Relation-based redactions ([\#2954](https://github.com/matrix-org/matrix-js-sdk/pull/2954)). + * Introduce a mechanism for using the rust-crypto-sdk ([\#2969](https://github.com/matrix-org/matrix-js-sdk/pull/2969)). + * Support MSC3391: Account data deletion ([\#2967](https://github.com/matrix-org/matrix-js-sdk/pull/2967)). + +## 🐛 Bug Fixes + * Fix threaded cache receipt when event holds multiple receipts ([\#3026](https://github.com/matrix-org/matrix-js-sdk/pull/3026)). + * Fix false key requests after verifying new device ([\#3029](https://github.com/matrix-org/matrix-js-sdk/pull/3029)). Fixes vector-im/element-web#24167 and vector-im/element-web#23333. + * Avoid triggering decryption errors when decrypting redacted events ([\#3004](https://github.com/matrix-org/matrix-js-sdk/pull/3004)). Fixes vector-im/element-web#24084. + * bugfix: upload OTKs in sliding sync mode ([\#3008](https://github.com/matrix-org/matrix-js-sdk/pull/3008)). + * Apply edits discovered from sync after thread is initialised ([\#3002](https://github.com/matrix-org/matrix-js-sdk/pull/3002)). Fixes vector-im/element-web#23921. + * Sliding sync: Fix issue where no unsubs are sent when switching rooms ([\#2991](https://github.com/matrix-org/matrix-js-sdk/pull/2991)). + * Threads are missing from the timeline ([\#2996](https://github.com/matrix-org/matrix-js-sdk/pull/2996)). Fixes vector-im/element-web#24036. + * Close all streams when a call ends ([\#2992](https://github.com/matrix-org/matrix-js-sdk/pull/2992)). Fixes vector-im/element-call#742. + * Resume to-device message queue after resumed sync ([\#2920](https://github.com/matrix-org/matrix-js-sdk/pull/2920)). Fixes matrix-org/element-web-rageshakes#17170. + * Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013. + * Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216. + * Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952. + Changes in [23.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.0.0) (2022-12-21) ================================================================================================== diff --git a/README.md b/README.md index 9f0e191ac67..71c8cba2ede 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ if you do not have it already. ```javascript import * as sdk from "matrix-js-sdk"; -const client = sdk.createClient("https://matrix.org"); +const client = sdk.createClient({ baseUrl: "https://matrix.org" }); client.publicRooms(function (err, data) { console.log("Public Rooms: %s", JSON.stringify(data)); }); diff --git a/examples/browser/README.md b/examples/browser/README.md index 3f52a4c4610..c7261b81b1d 100644 --- a/examples/browser/README.md +++ b/examples/browser/README.md @@ -1,6 +1,7 @@ To try it out, **you must build the SDK first** and then host this folder: ``` + $ yarn install $ yarn build $ cd examples/browser $ python -m http.server 8003 diff --git a/package.json b/package.json index 68c7102840b..0c5c9851618 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "23.0.0", + "version": "23.1.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=16.0.0" @@ -14,7 +14,7 @@ "build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", "build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly", "build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src", - "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", + "build:compile-browser": "mkdir dist && browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", "build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js", "gendoc": "typedoc", "lint": "yarn lint:types && yarn lint:js", @@ -33,10 +33,11 @@ "matrix-org" ], "main": "./lib/index.js", - "browser": "./lib/browser-index.ts", + "browser": "./lib/browser-index.js", "matrix_src_main": "./src/index.ts", "matrix_src_browser": "./src/browser-index.ts", "matrix_lib_main": "./lib/index.js", + "matrix_lib_browser": "./lib/browser-index.js", "matrix_lib_typings": "./lib/index.d.ts", "author": "matrix.org", "license": "Apache-2.0", @@ -54,6 +55,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", + "@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.2", "another-json": "^0.2.0", "bs58": "^5.0.0", "content-type": "^1.0.4", @@ -61,10 +63,9 @@ "matrix-events-sdk": "0.0.1", "matrix-widget-api": "^1.0.0", "p-retry": "4", - "qs": "^6.9.6", "sdp-transform": "^2.14.1", "unhomoglyph": "^1.0.6", - "uuid": "7" + "uuid": "9" }, "devDependencies": { "@babel/cli": "^7.12.10", @@ -97,7 +98,7 @@ "browserify": "^17.0.0", "docdash": "^2.0.0", "domexception": "^4.0.0", - "eslint": "8.28.0", + "eslint": "8.29.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-typescript": "^3.5.1", @@ -113,7 +114,7 @@ "jest-localstorage-mock": "^2.4.6", "jest-mock": "^29.0.0", "matrix-mock-request": "^2.5.0", - "prettier": "2.8.0", + "prettier": "2.8.1", "rimraf": "^3.0.2", "terser": "^5.5.1", "tsify": "^5.0.2", diff --git a/post-release.sh b/post-release.sh index 4e93f668a78..f42c1d6e067 100755 --- a/post-release.sh +++ b/post-release.sh @@ -21,9 +21,9 @@ if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then # to the TypeScript source. src_value=$(jq -r ".matrix_src_$i" package.json) if [ "$src_value" != "null" ]; then - jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json + jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json else - jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json + jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json fi fi done diff --git a/spec/TestClient.ts b/spec/TestClient.ts index 19a8d42c5e5..3e672bfb877 100644 --- a/spec/TestClient.ts +++ b/spec/TestClient.ts @@ -24,7 +24,7 @@ import MockHttpBackend from "matrix-mock-request"; import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store"; import { logger } from "../src/logger"; import { syncPromise } from "./test-utils/test-utils"; -import { createClient } from "../src/matrix"; +import { createClient, IStartClientOpts } from "../src/matrix"; import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client"; import { MockStorageApi } from "./MockStorageApi"; import { encodeUri } from "../src/utils"; @@ -79,9 +79,12 @@ export class TestClient { /** * start the client, and wait for it to initialise. */ - public start(): Promise { + public start(opts: IStartClientOpts = {}): Promise { logger.log(this + ": starting"); - this.httpBackend.when("GET", "/versions").respond(200, {}); + this.httpBackend.when("GET", "/versions").respond(200, { + // we have tests that rely on support for lazy-loading members + versions: ["r0.5.0"], + }); this.httpBackend.when("GET", "/pushrules").respond(200, {}); this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); this.expectDeviceKeyUpload(); @@ -93,6 +96,8 @@ export class TestClient { this.client.startClient({ // set this so that we can get hold of failed events pendingEventOrdering: PendingEventOrdering.Detached, + + ...opts, }); return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => { diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 9d71f060b48..0c553e77ff5 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -1073,6 +1073,97 @@ describe("MatrixClient event timelines", function () { }); }); + it("should ensure thread events are ordered correctly", async () => { + // Test data for a second reply to the first thread + const THREAD_REPLY2 = utils.mkEvent({ + room: roomId, + user: userId, + type: "m.room.message", + content: { + "body": "thread reply 2", + "msgtype": "m.text", + "m.relates_to": { + // We can't use the const here because we change server support mode for test + rel_type: "io.element.thread", + event_id: THREAD_ROOT.event_id, + }, + }, + event: true, + }); + THREAD_REPLY2.localTimestamp += 1000; + + // Test data for a second reply to the first thread + const THREAD_REPLY3 = utils.mkEvent({ + room: roomId, + user: userId, + type: "m.room.message", + content: { + "body": "thread reply 3", + "msgtype": "m.text", + "m.relates_to": { + // We can't use the const here because we change server support mode for test + rel_type: "io.element.thread", + event_id: THREAD_ROOT.event_id, + }, + }, + event: true, + }); + THREAD_REPLY3.localTimestamp += 2000; + + // Test data for the first thread, with the second reply + const THREAD_ROOT_UPDATED = { + ...THREAD_ROOT, + unsigned: { + ...THREAD_ROOT.unsigned, + "m.relations": { + ...THREAD_ROOT.unsigned!["m.relations"], + "io.element.thread": { + ...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"], + count: 3, + latest_event: THREAD_REPLY3.event, + }, + }, + }, + }; + + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Stable); + Thread.setServerSideListSupport(FeatureSupport.Stable); + Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable); + + client.fetchRoomEvent = () => Promise.resolve(THREAD_ROOT_UPDATED); + + await client.stopClient(); // we don't need the client to be syncing at this time + const room = client.getRoom(roomId)!; + + const prom = emitPromise(room, ThreadEvent.Update); + // Assume we're seeing the reply while loading backlog + room.addLiveEvents([THREAD_REPLY2]); + httpBackend + .when( + "GET", + "/_matrix/client/v1/rooms/!foo%3Abar/relations/" + + encodeURIComponent(THREAD_ROOT_UPDATED.event_id!) + + "/" + + encodeURIComponent(THREAD_RELATION_TYPE.name), + ) + .respond(200, { + chunk: [THREAD_REPLY3.event, THREAD_REPLY2.event, THREAD_REPLY], + }); + await flushHttp(prom); + // but while loading the metadata, a new reply has arrived + 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([ + THREAD_ROOT.event_id, + THREAD_REPLY.event_id, + THREAD_REPLY2.getId(), + THREAD_REPLY3.getId(), + ]); + }); + describe("paginateEventTimeline for thread list timeline", function () { const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index c83ada2d597..01e24ded4ea 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -35,9 +35,7 @@ describe("MatrixClient", function () { let store: MemoryStore | undefined; const defaultClientOpts: IStoredClientOpts = { - canResetEntireTimeline: (roomId) => false, experimentalThreadSupport: false, - crypto: {} as unknown as IStoredClientOpts["crypto"], }; const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => { const store = new MemoryStore(); @@ -1179,11 +1177,10 @@ describe("MatrixClient", function () { .when("PUT", "/send") .check((req) => { expect(req.data).toStrictEqual({ - "msgtype": "m.emote", - "body": "Body", - "formatted_body": "

Body

", - "format": "org.matrix.custom.html", - "org.matrix.msc1767.message": expect.anything(), + msgtype: "m.emote", + body: "Body", + formatted_body: "

Body

", + format: "org.matrix.custom.html", }); }) .respond(200, { event_id: "$foobar" }); @@ -1199,11 +1196,10 @@ describe("MatrixClient", function () { .when("PUT", "/send") .check((req) => { expect(req.data).toStrictEqual({ - "msgtype": "m.text", - "body": "Body", - "formatted_body": "

Body

", - "format": "org.matrix.custom.html", - "org.matrix.msc1767.message": expect.anything(), + msgtype: "m.text", + body: "Body", + formatted_body: "

Body

", + format: "org.matrix.custom.html", }); }) .respond(200, { event_id: "$foobar" }); diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index 0f92dc60d95..75487eb81dd 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -1543,6 +1543,52 @@ describe("MatrixClient syncing", () => { }); }); }); + + it("only replays receipts relevant to the current context", async () => { + const THREAD_ID = "$unknownthread:localhost"; + + const receipt = { + type: "m.receipt", + room_id: "!foo:bar", + content: { + "$event1:localhost": { + [ReceiptType.Read]: { + "@alice:localhost": { ts: 666, thread_id: THREAD_ID }, + }, + }, + "$otherevent:localhost": { + [ReceiptType.Read]: { + "@alice:localhost": { ts: 999, thread_id: "$otherthread:localhost" }, + }, + }, + }, + }; + syncData.rooms.join[roomOne].ephemeral.events = [receipt]; + + httpBackend!.when("GET", "/sync").respond(200, syncData); + client!.startClient(); + + return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]).then(() => { + const room = client?.getRoom(roomOne); + expect(room).toBeInstanceOf(Room); + + expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(true); + + const thread = room!.createThread(THREAD_ID, undefined, [], true); + + expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(false); + + const receipt = thread.getReadReceiptForUserId("@alice:localhost"); + + expect(receipt).toStrictEqual({ + data: { + thread_id: "$unknownthread:localhost", + ts: 666, + }, + eventId: "$event1:localhost", + }); + }); + }); }); describe("of a room", () => { diff --git a/spec/integ/megolm-integ.spec.ts b/spec/integ/megolm-integ.spec.ts index 334800333a6..1bdf3a09f47 100644 --- a/spec/integ/megolm-integ.spec.ts +++ b/spec/integ/megolm-integ.spec.ts @@ -1590,4 +1590,92 @@ describe("megolm", () => { aliceTestClient.httpBackend.flush("/send/m.room.encrypted/", 1), ]); }); + + describe("Lazy-loading member lists", () => { + let p2pSession: Olm.Session; + + beforeEach(async () => { + // set up the aliceTestClient so that it is a room with no known members + aliceTestClient.expectKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await aliceTestClient.start({ lazyLoadMembers: true }); + aliceTestClient.client.setGlobalErrorOnUnknownDevices(false); + + aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([])); + await aliceTestClient.flushSync(); + + p2pSession = await establishOlmSession(aliceTestClient, testOlmAccount); + }); + + async function expectMembershipRequest(roomId: string, members: string[]): Promise { + const membersPath = `/rooms/${encodeURIComponent(roomId)}/members?not_membership=leave`; + aliceTestClient.httpBackend.when("GET", membersPath).respond(200, { + chunk: [ + testUtils.mkMembershipCustom({ + membership: "join", + sender: "@bob:xyz", + }), + ], + }); + await aliceTestClient.httpBackend.flush(membersPath, 1); + } + + it("Sending an event initiates a member list sync", async () => { + // we expect a call to the /members list... + const memberListPromise = expectMembershipRequest(ROOM_ID, ["@bob:xyz"]); + + // then a request for bob's devices... + aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz")); + + // then a to-device with the room_key + const inboundGroupSessionPromise = expectSendRoomKey( + aliceTestClient.httpBackend, + "@bob:xyz", + testOlmAccount, + p2pSession, + ); + + // and finally the megolm message + const megolmMessagePromise = expectSendMegolmMessage( + aliceTestClient.httpBackend, + inboundGroupSessionPromise, + ); + + // kick it off + const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, "test"); + + await Promise.all([ + sendPromise, + megolmMessagePromise, + memberListPromise, + aliceTestClient.httpBackend.flush("/keys/query", 1), + ]); + }); + + it("loading the membership list inhibits a later load", async () => { + const room = aliceTestClient.client.getRoom(ROOM_ID)!; + await Promise.all([room.loadMembersIfNeeded(), expectMembershipRequest(ROOM_ID, ["@bob:xyz"])]); + + // expect a request for bob's devices... + aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz")); + + // then a to-device with the room_key + const inboundGroupSessionPromise = expectSendRoomKey( + aliceTestClient.httpBackend, + "@bob:xyz", + testOlmAccount, + p2pSession, + ); + + // and finally the megolm message + const megolmMessagePromise = expectSendMegolmMessage( + aliceTestClient.httpBackend, + inboundGroupSessionPromise, + ); + + // kick it off + const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, "test"); + + await Promise.all([sendPromise, megolmMessagePromise, aliceTestClient.httpBackend.flush("/keys/query", 1)]); + }); + }); }); diff --git a/spec/integ/rust-crypto.spec.ts b/spec/integ/rust-crypto.spec.ts new file mode 100644 index 00000000000..e018c210268 --- /dev/null +++ b/spec/integ/rust-crypto.spec.ts @@ -0,0 +1,90 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import "fake-indexeddb/auto"; +import { IDBFactory } from "fake-indexeddb"; + +import { createClient } 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(); +}); + +describe("MatrixClient.initRustCrypto", () => { + it("should raise if userId or deviceId is unknown", async () => { + const unknownUserClient = createClient({ + baseUrl: "http://test.server", + deviceId: "aliceDevice", + }); + await expect(() => unknownUserClient.initRustCrypto()).rejects.toThrow("unknown userId"); + + const unknownDeviceClient = createClient({ + baseUrl: "http://test.server", + userId: "@alice:test", + }); + await expect(() => unknownDeviceClient.initRustCrypto()).rejects.toThrow("unknown deviceId"); + }); + + it("should create the indexed dbs", async () => { + const matrixClient = createClient({ + baseUrl: "http://test.server", + userId: "@alice:localhost", + deviceId: "aliceDevice", + }); + + // No databases. + expect(await indexedDB.databases()).toHaveLength(0); + + await matrixClient.initRustCrypto(); + + // should have two dbs now + const databaseNames = (await indexedDB.databases()).map((db) => db.name); + expect(databaseNames).toEqual( + expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]), + ); + }); + + it("should ignore a second call", async () => { + const matrixClient = createClient({ + baseUrl: "http://test.server", + userId: "@alice:localhost", + deviceId: "aliceDevice", + }); + + await matrixClient.initRustCrypto(); + await matrixClient.initRustCrypto(); + }); +}); + +describe("MatrixClient.clearStores", () => { + it("should clear the indexeddbs", async () => { + const matrixClient = createClient({ + baseUrl: "http://test.server", + userId: "@alice:localhost", + deviceId: "aliceDevice", + }); + + await matrixClient.initRustCrypto(); + expect(await indexedDB.databases()).toHaveLength(2); + await matrixClient.stopClient(); + + await matrixClient.clearStores(); + expect(await indexedDB.databases()).toHaveLength(0); + }); +}); diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 165e3142012..a54cf71cb14 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -38,7 +38,7 @@ import { IRoomTimelineData, } from "../../src"; import { SlidingSyncSdk } from "../../src/sliding-sync-sdk"; -import { SyncState } from "../../src/sync"; +import { SyncApiOptions, SyncState } from "../../src/sync"; import { IStoredClientOpts } from "../../src/client"; import { logger } from "../../src/logger"; import { emitPromise } from "../test-utils/test-utils"; @@ -111,6 +111,7 @@ describe("SlidingSyncSdk", () => { // assign client/httpBackend globals const setupClient = async (testOpts?: Partial) => { testOpts = testOpts || {}; + const syncOpts: SyncApiOptions = {}; const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken); httpBackend = testClient.httpBackend; client = testClient.client; @@ -118,10 +119,10 @@ describe("SlidingSyncSdk", () => { if (testOpts.withCrypto) { httpBackend!.when("GET", "/room_keys/version").respond(404, {}); await client!.initCrypto(); - testOpts.crypto = client!.crypto; + syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto; } httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {}); - sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts); + sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts); }; // tear down client/httpBackend globals diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 4655ca0efd0..368ace4e80e 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -1418,6 +1418,102 @@ describe("SlidingSync", () => { await httpBackend!.flushAllExpected(); slidingSync.stop(); }); + + it("should not be possible to add/modify an already added custom subscription", async () => { + const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1); + slidingSync.addCustomSubscription(customSubName1, customSub1); + slidingSync.addCustomSubscription(customSubName1, customSub2); + slidingSync.useCustomSubscription(roomA, customSubName1); + slidingSync.modifyRoomSubscriptions(new Set([roomA])); + + httpBackend! + .when("POST", syncUrl) + .check(function (req) { + const body = req.data; + logger.log("custom subs", body); + expect(body.room_subscriptions).toBeTruthy(); + expect(body.room_subscriptions[roomA]).toEqual(customSub1); + }) + .respond(200, { + pos: "b", + lists: [], + extensions: {}, + rooms: {}, + }); + slidingSync.start(); + await httpBackend!.flushAllExpected(); + slidingSync.stop(); + }); + + it("should change the custom subscription if they are different", async () => { + const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1); + slidingSync.addCustomSubscription(customSubName1, customSub1); + slidingSync.addCustomSubscription(customSubName2, customSub2); + slidingSync.useCustomSubscription(roomA, customSubName1); + slidingSync.modifyRoomSubscriptions(new Set([roomA])); + + httpBackend! + .when("POST", syncUrl) + .check(function (req) { + const body = req.data; + logger.log("custom subs", body); + expect(body.room_subscriptions).toBeTruthy(); + expect(body.room_subscriptions[roomA]).toEqual(customSub1); + expect(body.unsubscribe_rooms).toBeUndefined(); + }) + .respond(200, { + pos: "b", + lists: [], + extensions: {}, + rooms: {}, + }); + slidingSync.start(); + await httpBackend!.flushAllExpected(); + + // using the same subscription doesn't unsub nor changes subscriptions + slidingSync.useCustomSubscription(roomA, customSubName1); + slidingSync.modifyRoomSubscriptions(new Set([roomA])); + + httpBackend! + .when("POST", syncUrl) + .check(function (req) { + const body = req.data; + logger.log("custom subs", body); + expect(body.room_subscriptions).toBeUndefined(); + expect(body.unsubscribe_rooms).toBeUndefined(); + }) + .respond(200, { + pos: "b", + lists: [], + extensions: {}, + rooms: {}, + }); + slidingSync.start(); + await httpBackend!.flushAllExpected(); + + // Changing the subscription works + slidingSync.useCustomSubscription(roomA, customSubName2); + slidingSync.modifyRoomSubscriptions(new Set([roomA])); + + httpBackend! + .when("POST", syncUrl) + .check(function (req) { + const body = req.data; + logger.log("custom subs", body); + expect(body.room_subscriptions).toBeTruthy(); + expect(body.room_subscriptions[roomA]).toEqual(customSub2); + expect(body.unsubscribe_rooms).toBeUndefined(); + }) + .respond(200, { + pos: "b", + lists: [], + extensions: {}, + rooms: {}, + }); + slidingSync.start(); + await httpBackend!.flushAllExpected(); + slidingSync.stop(); + }); }); describe("extensions", () => { diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index d8e350030f1..ed6e408ab1f 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.ts @@ -513,9 +513,6 @@ export class MockMatrixCall extends TypedEventEmitter(); - public on = jest.fn(); - public removeListener = jest.fn(); - public getOpponentMember(): Partial { return this.opponentMember; } diff --git a/spec/unit/ToDeviceMessageQueue.spec.ts b/spec/unit/ToDeviceMessageQueue.spec.ts new file mode 100644 index 00000000000..8752331c79e --- /dev/null +++ b/spec/unit/ToDeviceMessageQueue.spec.ts @@ -0,0 +1,106 @@ +import { ConnectionError } from "../../src/http-api/errors"; +import { ClientEvent, MatrixClient, Store } from "../../src/client"; +import { ToDeviceMessageQueue } from "../../src/ToDeviceMessageQueue"; +import { getMockClientWithEventEmitter } from "../test-utils/client"; +import { StubStore } from "../../src/store/stub"; +import { IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage"; +import { SyncState } from "../../src/sync"; + +describe("onResumedSync", () => { + let batch: IndexedToDeviceBatch | null; + let shouldFailSendToDevice: Boolean; + let onSendToDeviceFailure: () => void; + let onSendToDeviceSuccess: () => void; + let resumeSync: (newState: SyncState, oldState: SyncState) => void; + + let store: Store; + let mockClient: MatrixClient; + let queue: ToDeviceMessageQueue; + + beforeEach(() => { + batch = { + id: 0, + txnId: "123", + eventType: "m.dummy", + batch: [], + }; + + shouldFailSendToDevice = true; + onSendToDeviceFailure = () => {}; + onSendToDeviceSuccess = () => {}; + resumeSync = (newState, oldState) => { + shouldFailSendToDevice = false; + mockClient.emit(ClientEvent.Sync, newState, oldState); + }; + + store = new StubStore(); + store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => { + return batch; + }); + store.removeToDeviceBatch = jest.fn().mockImplementation(() => { + batch = null; + }); + + mockClient = getMockClientWithEventEmitter({}); + mockClient.store = store; + mockClient.sendToDevice = jest.fn().mockImplementation(async () => { + if (shouldFailSendToDevice) { + await Promise.reject(new ConnectionError("")).finally(() => { + setTimeout(onSendToDeviceFailure, 0); + }); + } else { + await Promise.resolve({}).finally(() => { + setTimeout(onSendToDeviceSuccess, 0); + }); + } + }); + + queue = new ToDeviceMessageQueue(mockClient); + }); + + it("resends queue after connectivity restored", (done) => { + onSendToDeviceFailure = () => { + expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1); + expect(store.removeToDeviceBatch).not.toHaveBeenCalled(); + + resumeSync(SyncState.Syncing, SyncState.Catchup); + expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(2); + }; + + onSendToDeviceSuccess = () => { + expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(3); + expect(store.removeToDeviceBatch).toHaveBeenCalled(); + done(); + }; + + queue.start(); + }); + + it("does not resend queue if client sync still catching up", (done) => { + onSendToDeviceFailure = () => { + expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1); + expect(store.removeToDeviceBatch).not.toHaveBeenCalled(); + + resumeSync(SyncState.Catchup, SyncState.Catchup); + expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1); + done(); + }; + + queue.start(); + }); + + it("does not resend queue if connectivity restored after queue stopped", (done) => { + onSendToDeviceFailure = () => { + expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1); + expect(store.removeToDeviceBatch).not.toHaveBeenCalled(); + + queue.stop(); + + resumeSync(SyncState.Syncing, SyncState.Catchup); + expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1); + done(); + }; + + queue.start(); + }); +}); diff --git a/spec/unit/autodiscovery.spec.ts b/spec/unit/autodiscovery.spec.ts index 0b6f8036d61..f6db5327c7b 100644 --- a/spec/unit/autodiscovery.spec.ts +++ b/spec/unit/autodiscovery.spec.ts @@ -544,7 +544,7 @@ describe("AutoDiscovery", function () { .respond(200, { versions: ["r0.0.1"], }); - httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {}); + httpBackend.when("GET", "/_matrix/identity/v2").respond(404, {}); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { "m.homeserver": { // Note: we also expect this test to trim the trailing slash @@ -591,7 +591,7 @@ describe("AutoDiscovery", function () { .respond(200, { versions: ["r0.0.1"], }); - httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {}); + httpBackend.when("GET", "/_matrix/identity/v2").respond(500, {}); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { "m.homeserver": { // Note: we also expect this test to trim the trailing slash @@ -636,9 +636,9 @@ describe("AutoDiscovery", function () { versions: ["r0.0.1"], }); httpBackend - .when("GET", "/_matrix/identity/api/v1") + .when("GET", "/_matrix/identity/v2") .check((req) => { - expect(req.path).toEqual("https://identity.example.org/_matrix/identity/api/v1"); + expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2"); }) .respond(200, {}); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { @@ -682,9 +682,9 @@ describe("AutoDiscovery", function () { versions: ["r0.0.1"], }); httpBackend - .when("GET", "/_matrix/identity/api/v1") + .when("GET", "/_matrix/identity/v2") .check((req) => { - expect(req.path).toEqual("https://identity.example.org/_matrix/identity/api/v1"); + expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2"); }) .respond(200, {}); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index 3b7689ca0ce..36a74607840 100644 --- a/spec/unit/crypto.spec.ts +++ b/spec/unit/crypto.spec.ts @@ -167,6 +167,38 @@ describe("Crypto", function () { client.stopClient(); }); + + it("doesn't throw an error when attempting to decrypt a redacted event", async () => { + const client = new TestClient("@alice:example.com", "deviceid").client; + await client.initCrypto(); + + const event = new MatrixEvent({ + content: {}, + event_id: "$event_id", + room_id: "!room_id", + sender: "@bob:example.com", + type: "m.room.encrypted", + unsigned: { + redacted_because: { + content: {}, + event_id: "$redaction_event_id", + redacts: "$event_id", + room_id: "!room_id", + origin_server_ts: 1234567890, + sender: "@bob:example.com", + type: "m.room.redaction", + unsigned: {}, + }, + }, + }); + await event.attemptDecryption(client.crypto!); + expect(event.isDecryptionFailure()).toBeFalsy(); + // since the redaction event isn't encrypted, the redacted_because + // should be the same as in the original event + expect(event.getRedactionEvent()).toEqual(event.getUnsigned().redacted_because); + + client.stopClient(); + }); }); describe("Session management", function () { diff --git a/spec/unit/crypto/verification/setDeviceVerification.spec.ts b/spec/unit/crypto/verification/setDeviceVerification.spec.ts deleted file mode 100644 index e1c07222663..00000000000 --- a/spec/unit/crypto/verification/setDeviceVerification.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import "../../../olm-loader"; - -import { CRYPTO_ENABLED, MatrixClient } from "../../../../src/client"; -import { TestClient } from "../../../TestClient"; - -const Olm = global.Olm; - -describe("crypto.setDeviceVerification", () => { - const userId = "@alice:example.com"; - const deviceId1 = "device1"; - let client: MatrixClient; - - if (!CRYPTO_ENABLED) { - return; - } - - beforeAll(async () => { - await Olm.init(); - }); - - beforeEach(async () => { - client = new TestClient(userId, deviceId1).client; - await client.initCrypto(); - }); - - it("client should provide crypto", () => { - expect(client.crypto).not.toBeUndefined(); - }); - - describe("when setting an own device as verified", () => { - beforeEach(async () => { - jest.spyOn(client.crypto!, "cancelAndResendAllOutgoingKeyRequests"); - await client.crypto!.setDeviceVerification(userId, deviceId1, true); - }); - - it("cancelAndResendAllOutgoingKeyRequests should be called", () => { - expect(client.crypto!.cancelAndResendAllOutgoingKeyRequests).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/unit/event-timeline-set.spec.ts b/spec/unit/event-timeline-set.spec.ts index 2831b6ca608..05b80fe57de 100644 --- a/spec/unit/event-timeline-set.spec.ts +++ b/spec/unit/event-timeline-set.spec.ts @@ -24,10 +24,13 @@ import { MatrixClient, MatrixEvent, MatrixEventEvent, + RelationType, Room, + RoomEvent, } from "../../src"; -import { Thread } from "../../src/models/thread"; +import { FeatureSupport, Thread } from "../../src/models/thread"; import { ReEmitter } from "../../src/ReEmitter"; +import { eventMapperFor } from "../../src/event-mapper"; describe("EventTimelineSet", () => { const roomId = "!foo:bar"; @@ -202,6 +205,88 @@ describe("EventTimelineSet", () => { expect(liveTimeline.getEvents().length).toStrictEqual(0); }); + it("should allow edits to be added to thread timeline", async () => { + jest.spyOn(client, "supportsExperimentalThreads").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/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index b13dfc125be..b692e10ce22 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -22,11 +22,13 @@ import { Filter } from "../../src/filter"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace"; import { EventType, + RelationType, RoomCreateTypeField, RoomType, UNSTABLE_MSC3088_ENABLED, UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, + MSC3912_RELATION_BASED_REDACTIONS_PROP, } from "../../src/@types/event"; import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib"; import { Crypto } from "../../src/crypto"; @@ -38,6 +40,8 @@ import { makeBeaconInfoContent } from "../../src/content-helpers"; import { M_BEACON_INFO } from "../../src/@types/beacon"; import { ContentHelpers, + ClientPrefix, + Direction, EventTimeline, ICreateRoomOpts, IRequestOpts, @@ -57,6 +61,8 @@ import { import { IOlmDevice } from "../../src/crypto/algorithms/megolm"; import { QueryDict } from "../../src/utils"; import { SyncState } from "../../src/sync"; +import * as featureUtils from "../../src/feature"; +import { StubStore } from "../../src/store/stub"; jest.useFakeTimers(); @@ -65,9 +71,20 @@ jest.mock("../../src/webrtc/call", () => ({ supportsMatrixCall: jest.fn(() => false), })); +// Utility function to ease the transition from our QueryDict type to a Map +// which we can use to build a URLSearchParams +function convertQueryDictToMap(queryDict?: QueryDict): Map { + if (!queryDict) { + return new Map(); + } + + return new Map(Object.entries(queryDict).map(([k, v]) => [k, String(v)])); +} + type HttpLookup = { method: string; path: string; + prefix?: string; data?: Record; error?: object; expectBody?: Record; @@ -84,6 +101,42 @@ type WrappedRoom = Room & { _state: Map; }; +describe("convertQueryDictToMap", () => { + it("returns an empty map when dict is undefined", () => { + expect(convertQueryDictToMap(undefined)).toEqual(new Map()); + }); + + it("converts an empty QueryDict to an empty map", () => { + expect(convertQueryDictToMap({})).toEqual(new Map()); + }); + + it("converts a QueryDict of strings to the equivalent map", () => { + expect(convertQueryDictToMap({ a: "b", c: "d" })).toEqual( + new Map([ + ["a", "b"], + ["c", "d"], + ]), + ); + }); + + it("converts the values of the supplied QueryDict to strings", () => { + expect(convertQueryDictToMap({ arr: ["b", "c"], num: 45, boo: true, und: undefined })).toEqual( + new Map([ + ["arr", "b,c"], + ["num", "45"], + ["boo", "true"], + ["und", "undefined"], + ]), + ); + }); + + it("produces sane URLSearchParams conversions", () => { + expect(new URLSearchParams(Array.from(convertQueryDictToMap({ a: "b", c: "d" }))).toString()).toEqual( + "a=b&c=d", + ); + }); +}); + describe("MatrixClient", function () { const userId = "@alice:bar"; const identityServerUrl = "https://identity.server"; @@ -120,6 +173,10 @@ describe("MatrixClient", function () { data: SYNC_DATA, }; + const unstableFeatures: Record = { + "org.matrix.msc3440.stable": true, + }; + // items are popped off when processed and block if no items left. let httpLookups: HttpLookup[] = []; let acceptKeepalives: boolean; @@ -128,12 +185,17 @@ describe("MatrixClient", function () { method: string; path: string; } | null = null; - function httpReq(method: Method, path: string, qp?: QueryDict, data?: BodyInit, opts?: IRequestOpts) { + function httpReq( + method: Method, + path: string, + queryParams?: QueryDict, + body?: BodyInit, + requestOpts: IRequestOpts = {}, + ) { + const { prefix } = requestOpts; if (path === KEEP_ALIVE_PATH && acceptKeepalives) { return Promise.resolve({ - unstable_features: { - "org.matrix.msc3440.stable": true, - }, + unstable_features: unstableFeatures, versions: ["r0.6.0", "r0.6.1"], }); } @@ -166,14 +228,17 @@ describe("MatrixClient", function () { }; return pendingLookup.promise; } - if (next.path === path && next.method === method) { + // Either we don't care about the prefix if it wasn't defined in the expected + // lookup or it should match. + const doesMatchPrefix = !next.prefix || next.prefix === prefix; + if (doesMatchPrefix && next.path === path && next.method === method) { logger.log("MatrixClient[UT] Matched. Returning " + (next.error ? "BAD" : "GOOD") + " response"); if (next.expectBody) { - expect(data).toEqual(next.expectBody); + expect(body).toEqual(next.expectBody); } if (next.expectQueryParams) { Object.keys(next.expectQueryParams).forEach(function (k) { - expect(qp?.[k]).toEqual(next.expectQueryParams![k]); + expect(queryParams?.[k]).toEqual(next.expectQueryParams![k]); }); } @@ -193,12 +258,22 @@ describe("MatrixClient", function () { } return Promise.resolve(next.data); } - // Jest doesn't let us have custom expectation errors, so if you're seeing this then - // you forgot to handle at least 1 pending request. Check your tests to ensure your - // number of expectations lines up with your number of requests made, and that those - // requests match your expectations. - expect(true).toBe(false); - return new Promise(() => {}); + + const receivedRequestQueryString = new URLSearchParams( + Array.from(convertQueryDictToMap(queryParams)), + ).toString(); + const receivedRequestDebugString = `${method} ${prefix}${path}${receivedRequestQueryString}`; + const expectedQueryString = new URLSearchParams( + Array.from(convertQueryDictToMap(next.expectQueryParams)), + ).toString(); + const expectedRequestDebugString = `${next.method} ${next.prefix ?? ""}${next.path}${expectedQueryString}`; + // If you're seeing this then you forgot to handle at least 1 pending request. + throw new Error( + `A pending request was not handled: ${receivedRequestDebugString} ` + + `(next request expected was ${expectedRequestDebugString})\n` + + `Check your tests to ensure your number of expectations lines up with your number of requests ` + + `made, and that those requests match your expectations.`, + ); } function makeClient() { @@ -281,6 +356,183 @@ describe("MatrixClient", function () { client.stopClient(); }); + describe("timestampToEvent", () => { + const roomId = "!room:server.org"; + const eventId = "$eventId:example.org"; + const unstableMSC3030Prefix = "/_matrix/client/unstable/org.matrix.msc3030"; + + async function assertRequestsMade( + responses: { + prefix?: string; + error?: { httpStatus: Number; errcode: string }; + data?: { event_id: string }; + }[], + expectRejects = false, + ) { + const queryParams = { + ts: "0", + dir: "f", + }; + const path = `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`; + // Set up the responses we are going to send back + httpLookups = responses.map((res) => { + return { + method: "GET", + path, + expectQueryParams: queryParams, + ...res, + }; + }); + + // When we ask for the event timestamp (this is what we are testing) + const answer = client.timestampToEvent(roomId, 0, Direction.Forward); + + if (expectRejects) { + await expect(answer).rejects.toBeDefined(); + } else { + await answer; + } + + // Then the number of requests me made matches our expectation + const calls = mocked(client.http.authedRequest).mock.calls; + expect(calls.length).toStrictEqual(responses.length); + + // And each request was as we expected + let i = 0; + for (const call of calls) { + const response = responses[i]; + const [callMethod, callPath, callQueryParams, , callOpts] = call; + const callPrefix = callOpts?.prefix; + + expect(callMethod).toStrictEqual("GET"); + if (response.prefix) { + expect(callPrefix).toStrictEqual(response.prefix); + } + expect(callPath).toStrictEqual(path); + expect(callQueryParams).toStrictEqual(queryParams); + i++; + } + } + + it("should call stable endpoint", async () => { + await assertRequestsMade([ + { + data: { event_id: eventId }, + }, + ]); + }); + + it("should fallback to unstable endpoint when stable endpoint 400s", async () => { + await assertRequestsMade([ + { + prefix: ClientPrefix.V1, + error: { + httpStatus: 400, + errcode: "M_UNRECOGNIZED", + }, + }, + { + prefix: unstableMSC3030Prefix, + data: { event_id: eventId }, + }, + ]); + }); + + it("should fallback to unstable endpoint when stable endpoint 404s", async () => { + await assertRequestsMade([ + { + prefix: ClientPrefix.V1, + error: { + httpStatus: 404, + errcode: "M_UNRECOGNIZED", + }, + }, + { + prefix: unstableMSC3030Prefix, + data: { event_id: eventId }, + }, + ]); + }); + + it("should fallback to unstable endpoint when stable endpoint 405s", async () => { + await assertRequestsMade([ + { + prefix: ClientPrefix.V1, + error: { + httpStatus: 405, + errcode: "M_UNRECOGNIZED", + }, + }, + { + prefix: unstableMSC3030Prefix, + data: { event_id: eventId }, + }, + ]); + }); + + it("should not fallback to unstable endpoint when stable endpoint returns an error (500)", async () => { + await assertRequestsMade( + [ + { + prefix: ClientPrefix.V1, + error: { + httpStatus: 500, + errcode: "Fake response error", + }, + }, + ], + true, + ); + }); + + it("should not fallback to unstable endpoint when stable endpoint is rate-limiting (429)", async () => { + await assertRequestsMade( + [ + { + prefix: ClientPrefix.V1, + error: { + httpStatus: 429, + errcode: "M_UNRECOGNIZED", // Still refuses even if the errcode claims unrecognised + }, + }, + ], + true, + ); + }); + + it("should not fallback to unstable endpoint when stable endpoint says bad gateway (502)", async () => { + await assertRequestsMade( + [ + { + prefix: ClientPrefix.V1, + error: { + httpStatus: 502, + errcode: "Fake response error", + }, + }, + ], + true, + ); + }); + }); + + describe("getSafeUserId()", () => { + it("returns the logged in user id", () => { + expect(client.getSafeUserId()).toEqual(userId); + }); + + it("throws when there is not logged in user", () => { + const notLoggedInClient = new MatrixClient({ + baseUrl: "https://my.home.server", + idBaseUrl: identityServerUrl, + fetchFn: function () {} as any, // NOP + store: store, + scheduler: scheduler, + }); + expect(() => notLoggedInClient.getSafeUserId()).toThrow("Expected logged in user but found none."); + }); + }); + describe("sendEvent", () => { const roomId = "!room:example.org"; const body = "This is the body"; @@ -1067,6 +1319,59 @@ describe("MatrixClient", function () { await client.redactEvent(roomId, eventId, txnId, { reason }); }); + + describe("when calling with with_relations", () => { + const eventId = "$event42:example.org"; + + it("should raise an error if server has no support for relation based redactions", async () => { + // load supported features + await client.getVersions(); + + const txnId = client.makeTxnId(); + + expect(() => { + client.redactEvent(roomId, eventId, txnId, { + with_relations: [RelationType.Reference], + }); + }).toThrowError( + new Error( + "Server does not support relation based redactions " + + `roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId null`, + ), + ); + }); + + describe("and the server supports relation based redactions (unstable)", () => { + beforeEach(async () => { + unstableFeatures["org.matrix.msc3912"] = true; + // load supported features + await client.getVersions(); + }); + + it("should send with_relations in the request body", async () => { + const txnId = client.makeTxnId(); + + httpLookups = [ + { + method: "PUT", + path: + `/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}` + + `/${encodeURIComponent(txnId)}`, + expectBody: { + reason: "redaction test", + [MSC3912_RELATION_BASED_REDACTIONS_PROP.unstable!]: [RelationType.Reference], + }, + data: { event_id: eventId }, + }, + ]; + + await client.redactEvent(roomId, eventId, txnId, { + reason: "redaction test", + with_relations: [RelationType.Reference], + }); + }); + }); + }); }); describe("cancelPendingEvent", () => { @@ -1828,4 +2133,146 @@ describe("MatrixClient", function () { expect(client.getUseE2eForGroupCall()).toBe(false); }); }); + + describe("delete account data", () => { + afterEach(() => { + jest.spyOn(featureUtils, "buildFeatureSupportMap").mockRestore(); + }); + it("makes correct request when deletion is supported by server in unstable versions", async () => { + const eventType = "im.vector.test"; + const versionsResponse = { + versions: ["1"], + unstable_features: { + "org.matrix.msc3391": true, + }, + }; + jest.spyOn(client.http, "request").mockResolvedValue(versionsResponse); + const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve()); + const unstablePrefix = "/_matrix/client/unstable/org.matrix.msc3391"; + const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`; + + // populate version support + await client.getVersions(); + await client.deleteAccountData(eventType); + + expect(requestSpy).toHaveBeenCalledWith(Method.Delete, path, undefined, undefined, { + prefix: unstablePrefix, + }); + }); + + it("makes correct request when deletion is supported by server based on matrix version", async () => { + const eventType = "im.vector.test"; + // we don't have a stable version for account data deletion yet to test this code path with + // so mock the support map to fake stable support + const stableSupportedDeletionMap = new Map(); + stableSupportedDeletionMap.set(featureUtils.Feature.AccountDataDeletion, featureUtils.ServerSupport.Stable); + jest.spyOn(featureUtils, "buildFeatureSupportMap").mockResolvedValue(new Map()); + const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve()); + const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`; + + // populate version support + await client.getVersions(); + await client.deleteAccountData(eventType); + + expect(requestSpy).toHaveBeenCalledWith(Method.Delete, path, undefined, undefined, undefined); + }); + + it("makes correct request when deletion is not supported by server", async () => { + const eventType = "im.vector.test"; + const versionsResponse = { + versions: ["1"], + unstable_features: { + "org.matrix.msc3391": false, + }, + }; + jest.spyOn(client.http, "request").mockResolvedValue(versionsResponse); + const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve()); + const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`; + + // populate version support + await client.getVersions(); + await client.deleteAccountData(eventType); + + // account data updated with empty content + expect(requestSpy).toHaveBeenCalledWith(Method.Put, path, undefined, {}); + }); + }); + + describe("getVisibleRooms", () => { + function roomCreateEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent { + return new MatrixEvent({ + content: { + "creator": "@daryl:alexandria.example.com", + "m.federate": true, + "predecessor": { + event_id: "spec_is_not_clear_what_id_this_is", + room_id: predecessorRoomId, + }, + "room_version": "9", + }, + event_id: `create_event_id_pred_${predecessorRoomId}`, + origin_server_ts: 1432735824653, + room_id: newRoomId, + sender: "@daryl:alexandria.example.com", + state_key: "", + type: "m.room.create", + }); + } + + function tombstoneEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent { + return new MatrixEvent({ + content: { + body: "This room has been replaced", + replacement_room: newRoomId, + }, + event_id: `tombstone_event_id_pred_${predecessorRoomId}`, + origin_server_ts: 1432735824653, + room_id: predecessorRoomId, + sender: "@daryl:alexandria.example.com", + state_key: "", + type: "m.room.tombstone", + }); + } + + it("Returns an empty list if there are no rooms", () => { + client.store = new StubStore(); + client.store.getRooms = () => []; + const rooms = client.getVisibleRooms(); + expect(rooms).toHaveLength(0); + }); + + it("Returns all non-replaced rooms", () => { + const room1 = new Room("room1", client, "@carol:alexandria.example.com"); + const room2 = new Room("room2", client, "@daryl:alexandria.example.com"); + client.store = new StubStore(); + client.store.getRooms = () => [room1, room2]; + const rooms = client.getVisibleRooms(); + expect(rooms).toContain(room1); + expect(rooms).toContain(room2); + expect(rooms).toHaveLength(2); + }); + + it("Does not return replaced rooms", () => { + // Given 4 rooms, 2 of which have been replaced + const room1 = new Room("room1", client, "@carol:alexandria.example.com"); + const replacedRoom1 = new Room("replacedRoom1", client, "@carol:alexandria.example.com"); + const replacedRoom2 = new Room("replacedRoom2", client, "@carol:alexandria.example.com"); + const room2 = new Room("room2", client, "@daryl:alexandria.example.com"); + client.store = new StubStore(); + client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2]; + room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], {}); + room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], {}); + replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], {}); + replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], {}); + + // When we ask for the visible rooms + const rooms = client.getVisibleRooms(); + + // Then we only get the ones that have not been replaced + expect(rooms).not.toContain(replacedRoom1); + expect(rooms).not.toContain(replacedRoom2); + expect(rooms).toContain(room1); + expect(rooms).toContain(room2); + }); + }); }); diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index da492b54225..244f9214521 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -16,7 +16,6 @@ limitations under the License. import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event"; import { emitPromise } from "../../test-utils/test-utils"; -import { EventType } from "../../../src"; import { Crypto } from "../../../src/crypto"; describe("MatrixEvent", () => { @@ -88,22 +87,6 @@ describe("MatrixEvent", () => { expect(ev.getWireContent().ciphertext).toBeUndefined(); }); - it("should abort decryption if fails with an error other than a DecryptionError", async () => { - const ev = new MatrixEvent({ - type: EventType.RoomMessageEncrypted, - content: { - body: "Test", - }, - event_id: "$event1:server", - }); - await ev.attemptDecryption({ - decryptEvent: jest.fn().mockRejectedValue(new Error("Not a DecryptionError")), - } as unknown as Crypto); - expect(ev.isEncrypted()).toBeTruthy(); - expect(ev.isBeingDecrypted()).toBeFalsy(); - expect(ev.isDecryptionFailure()).toBeFalsy(); - }); - describe("applyVisibilityEvent", () => { it("should emit VisibilityChange if a change was made", async () => { const ev = new MatrixEvent({ @@ -134,6 +117,21 @@ describe("MatrixEvent", () => { }); }); + it("should report decryption errors", async () => { + const crypto = { + decryptEvent: jest.fn().mockRejectedValue(new Error("test error")), + } as unknown as Crypto; + + await encryptedEvent.attemptDecryption(crypto); + expect(encryptedEvent.isEncrypted()).toBeTruthy(); + expect(encryptedEvent.isBeingDecrypted()).toBeFalsy(); + expect(encryptedEvent.isDecryptionFailure()).toBeTruthy(); + expect(encryptedEvent.getContent()).toEqual({ + msgtype: "m.bad.encrypted", + body: "** Unable to decrypt: Error: test error **", + }); + }); + it("should retry decryption if a retry is queued", async () => { const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption"); diff --git a/spec/unit/models/thread.spec.ts b/spec/unit/models/thread.spec.ts index 42b976cd6ac..4dd5a681b6e 100644 --- a/spec/unit/models/thread.spec.ts +++ b/spec/unit/models/thread.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 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. @@ -19,8 +19,12 @@ import { Room } from "../../../src/models/room"; import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread"; import { mkThread } from "../../test-utils/thread"; import { TestClient } from "../../TestClient"; -import { emitPromise, mkMessage } from "../../test-utils/test-utils"; -import { EventStatus } from "../../../src"; +import { emitPromise, mkMessage, mock } from "../../test-utils/test-utils"; +import { Direction, EventStatus, 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"; describe("Thread", () => { describe("constructor", () => { @@ -71,17 +75,54 @@ describe("Thread", () => { }); describe("hasUserReadEvent", () => { - const myUserId = "@bob:example.org"; + let myUserId: string; let client: MatrixClient; let room: Room; beforeEach(() => { - const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, { - timelineSupport: false, + client = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(), + getRoom: jest.fn().mockImplementation(() => room), + decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0), + supportsExperimentalThreads: jest.fn().mockReturnValue(true), + }); + client.reEmitter = mock(ReEmitter, "ReEmitter"); + client.canSupport = new Map(); + Object.keys(Feature).forEach((feature) => { + client.canSupport.set(feature as Feature, ServerSupport.Stable); }); - client = testClient.client; + + myUserId = client.getUserId()!; + room = new Room("123", client, myUserId); + const receipt = new MatrixEvent({ + type: "m.receipt", + room_id: "!foo:bar", + content: { + // first threaded receipt + "$event0:localhost": { + [ReceiptType.Read]: { + [client.getUserId()!]: { ts: 100, thread_id: "$threadId:localhost" }, + }, + }, + // last unthreaded receipt + "$event1:localhost": { + [ReceiptType.Read]: { + [client.getUserId()!]: { ts: 200 }, + ["@alice:example.org"]: { ts: 200 }, + }, + }, + // last threaded receipt + "$event2:localhost": { + [ReceiptType.Read]: { + [client.getUserId()!]: { ts: 300, thread_id: "$threadId" }, + }, + }, + }, + }); + room.addReceipt(receipt); + jest.spyOn(client, "getRoom").mockReturnValue(room); }); @@ -98,19 +139,287 @@ describe("Thread", () => { length: 2, }); + // The event is automatically considered read as the current user is the sender expect(thread.hasUserReadEvent(myUserId, events.at(-1)!.getId() ?? "")).toBeTruthy(); }); it("considers other events with no RR as unread", () => { + const { thread, events } = mkThread({ + room, + client, + authorId: myUserId, + participantUserIds: [myUserId], + length: 25, + ts: 190, + }); + + // Before alice's last unthreaded receipt + expect(thread.hasUserReadEvent("@alice:example.org", events.at(1)!.getId() ?? "")).toBeTruthy(); + + // After alice's last unthreaded receipt + expect(thread.hasUserReadEvent("@alice:example.org", events.at(-1)!.getId() ?? "")).toBeFalsy(); + }); + + it("considers event as read if there's a more recent unthreaded receipt", () => { const { thread, events } = mkThread({ room, client, authorId: myUserId, participantUserIds: ["@alice:example.org"], length: 2, + ts: 150, // before the latest unthreaded receipt }); + expect(thread.hasUserReadEvent(client.getUserId()!, events.at(-1)!.getId() ?? "")).toBe(true); + }); - expect(thread.hasUserReadEvent("@alice:example.org", events.at(-1)!.getId() ?? "")).toBeFalsy(); + it("considers event as unread if there's no more recent unthreaded receipt", () => { + const { thread, events } = mkThread({ + room, + client, + authorId: myUserId, + participantUserIds: ["@alice:example.org"], + length: 2, + ts: 1000, + }); + expect(thread.hasUserReadEvent(client.getUserId()!, events.at(-1)!.getId() ?? "")).toBe(false); + }); + }); + + describe("getEventReadUpTo", () => { + let myUserId: string; + let client: MatrixClient; + let room: Room; + + beforeEach(() => { + client = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(), + getRoom: jest.fn().mockImplementation(() => room), + decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0), + supportsExperimentalThreads: jest.fn().mockReturnValue(true), + }); + client.reEmitter = mock(ReEmitter, "ReEmitter"); + client.canSupport = new Map(); + Object.keys(Feature).forEach((feature) => { + client.canSupport.set(feature as Feature, ServerSupport.Stable); + }); + + myUserId = client.getUserId()!; + + room = new Room("123", client, myUserId); + + jest.spyOn(client, "getRoom").mockReturnValue(room); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it("uses unthreaded receipt to figure out read up to", () => { + const receipt = new MatrixEvent({ + type: "m.receipt", + room_id: "!foo:bar", + content: { + // last unthreaded receipt + "$event1:localhost": { + [ReceiptType.Read]: { + ["@alice:example.org"]: { ts: 200 }, + }, + }, + }, + }); + room.addReceipt(receipt); + + const { thread, events } = mkThread({ + room, + client, + authorId: myUserId, + participantUserIds: [myUserId], + length: 25, + ts: 190, + }); + + // The 10th event has been read, as alice's last unthreaded receipt is at ts 200 + // and `mkThread` increment every thread response by 1ms. + expect(thread.getEventReadUpTo("@alice:example.org")).toBe(events.at(9)!.getId()); + }); + + it("considers thread created before the first threaded receipt to be read", () => { + const receipt = new MatrixEvent({ + type: "m.receipt", + room_id: "!foo:bar", + content: { + // last unthreaded receipt + "$event1:localhost": { + [ReceiptType.Read]: { + [myUserId]: { ts: 200, thread_id: "$threadId" }, + }, + }, + }, + }); + room.addReceipt(receipt); + + const { thread, events } = mkThread({ + room, + client, + authorId: "@alice:example.org", + participantUserIds: ["@alice:example.org"], + length: 2, + ts: 10, + }); + + // This is marked as read as it is before alice's first threaded receipt... + expect(thread.getEventReadUpTo(myUserId)).toBe(events.at(-1)!.getId()); + + const { thread: thread2 } = mkThread({ + room, + client, + authorId: "@alice:example.org", + participantUserIds: ["@alice:example.org"], + length: 2, + ts: 1000, + }); + + // Nothing has been read, this thread is after the first threaded receipt... + expect(thread2.getEventReadUpTo(myUserId)).toBe(null); + }); + }); + + describe("resetLiveTimeline", () => { + // ResetLiveTimeline is used when we have missing messages between the current live timeline's end and newly + // received messages. In that case, we want to replace the existing live timeline. To ensure pagination + // continues working correctly, new pagination tokens need to be set on both the old live timeline (which is + // now a regular timeline) and the new live timeline. + it("replaces the live timeline and correctly sets pagination tokens", async () => { + const myUserId = "@bob:example.org"; + const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, { + timelineSupport: false, + }); + const client = testClient.client; + const room = new Room("123", client, myUserId, { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + jest.spyOn(client, "getRoom").mockReturnValue(room); + + const { thread } = mkThread({ + room, + client, + authorId: myUserId, + participantUserIds: ["@alice:example.org"], + length: 3, + }); + await emitPromise(thread, ThreadEvent.Update); + expect(thread.length).toBe(2); + + jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) => + Promise.resolve({ + chunk: [], + start: `${token}-new`, + end: `${token}-new`, + }), + ); + + function timelines(): [string | null, string | null][] { + return thread.timelineSet + .getTimelines() + .map((it) => [it.getPaginationToken(Direction.Backward), it.getPaginationToken(Direction.Forward)]); + } + + expect(timelines()).toEqual([[null, null]]); + const promise = thread.resetLiveTimeline("b1", "f1"); + expect(timelines()).toEqual([ + [null, "f1"], + ["b1", null], + ]); + await promise; + expect(timelines()).toEqual([ + [null, "f1-new"], + ["b1-new", null], + ]); + }); + + // As the pagination tokens cannot be used right now, resetLiveTimeline needs to replace them before they can + // be used. But if in the future the bug in synapse is fixed, and they can actually be used, we can get into a + // state where the client has paginated (and changed the tokens) while resetLiveTimeline tries to set the + // corrected tokens. To prevent such a race condition, we make sure that resetLiveTimeline respects any + // changes done to the pagination tokens. + it("replaces the live timeline but does not replace changed pagination tokens", async () => { + const myUserId = "@bob:example.org"; + const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, { + timelineSupport: false, + }); + const client = testClient.client; + const room = new Room("123", client, myUserId, { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + jest.spyOn(client, "getRoom").mockReturnValue(room); + + const { thread } = mkThread({ + room, + client, + authorId: myUserId, + participantUserIds: ["@alice:example.org"], + length: 3, + }); + await emitPromise(thread, ThreadEvent.Update); + expect(thread.length).toBe(2); + + jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) => + Promise.resolve({ + chunk: [], + start: `${token}-new`, + end: `${token}-new`, + }), + ); + + function timelines(): [string | null, string | null][] { + return thread.timelineSet + .getTimelines() + .map((it) => [it.getPaginationToken(Direction.Backward), it.getPaginationToken(Direction.Forward)]); + } + + expect(timelines()).toEqual([[null, null]]); + const promise = thread.resetLiveTimeline("b1", "f1"); + expect(timelines()).toEqual([ + [null, "f1"], + ["b1", null], + ]); + thread.timelineSet.getTimelines()[0].setPaginationToken("f2", Direction.Forward); + thread.timelineSet.getTimelines()[1].setPaginationToken("b2", Direction.Backward); + await promise; + expect(timelines()).toEqual([ + [null, "f2"], + ["b2", null], + ]); + }); + + it("is correctly called by the room", async () => { + const myUserId = "@bob:example.org"; + const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, { + timelineSupport: false, + }); + const client = testClient.client; + const room = new Room("123", client, myUserId, { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + jest.spyOn(client, "getRoom").mockReturnValue(room); + + const { thread } = mkThread({ + room, + client, + authorId: myUserId, + participantUserIds: ["@alice:example.org"], + length: 3, + }); + await emitPromise(thread, ThreadEvent.Update); + expect(thread.length).toBe(2); + const mock = jest.spyOn(thread, "resetLiveTimeline"); + mock.mockReturnValue(Promise.resolve()); + + room.resetLiveTimeline("b1", "f1"); + expect(mock).toHaveBeenCalledWith("b1", "f1"); }); }); }); diff --git a/spec/unit/notifications.spec.ts b/spec/unit/notifications.spec.ts index 144afb70f12..594740e285a 100644 --- a/spec/unit/notifications.spec.ts +++ b/spec/unit/notifications.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 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. @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ReceiptType } from "../../src/@types/read_receipts"; import { Feature, ServerSupport } from "../../src/feature"; import { EventType, @@ -64,6 +65,30 @@ describe("fixNotificationCountOnDecryption", () => { }); room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? ""); + + const receipt = new MatrixEvent({ + type: "m.receipt", + room_id: "!foo:bar", + content: { + "$event0:localhost": { + [ReceiptType.Read]: { + [mockClient.getUserId()!]: { ts: 123 }, + }, + }, + "$event1:localhost": { + [ReceiptType.Read]: { + [mockClient.getUserId()!]: { ts: 666, thread_id: THREAD_ID }, + }, + }, + "$otherevent:localhost": { + [ReceiptType.Read]: { + [mockClient.getUserId()!]: { ts: 999, thread_id: "$otherthread:localhost" }, + }, + }, + }, + }); + room.addReceipt(receipt); + room.setUnreadNotificationCount(NotificationCountType.Total, 1); room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); @@ -75,6 +100,7 @@ describe("fixNotificationCountOnDecryption", () => { body: "Hello world!", }, event: true, + ts: 1234, }, mockClient, ); @@ -90,6 +116,7 @@ describe("fixNotificationCountOnDecryption", () => { "msgtype": MsgType.Text, "body": "Thread reply", }, + ts: 5678, event: true, }); room.createThread(THREAD_ID, event, [threadEvent], false); @@ -155,6 +182,7 @@ describe("fixNotificationCountOnDecryption", () => { "msgtype": MsgType.Text, "body": "Thread reply", }, + ts: 8901, event: true, }); diff --git a/spec/unit/relations.spec.ts b/spec/unit/relations.spec.ts index 91b77dd1212..cf4997c2809 100644 --- a/spec/unit/relations.spec.ts +++ b/spec/unit/relations.spec.ts @@ -14,13 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { M_POLL_START } from "matrix-events-sdk"; + import { EventTimelineSet } from "../../src/models/event-timeline-set"; import { MatrixEvent, MatrixEventEvent } from "../../src/models/event"; import { Room } from "../../src/models/room"; -import { Relations } from "../../src/models/relations"; +import { Relations, RelationsEvent } from "../../src/models/relations"; import { TestClient } from "../TestClient"; +import { RelationType } from "../../src"; +import { logger } from "../../src/logger"; describe("Relations", function () { + afterEach(() => { + jest.spyOn(logger, "error").mockRestore(); + }); + it("should deduplicate annotations", function () { const room = new Room("room123", null!, null!); const relations = new Relations("m.annotation", "m.reaction", room); @@ -75,6 +83,92 @@ describe("Relations", function () { } }); + describe("addEvent()", () => { + const relationType = RelationType.Reference; + const eventType = M_POLL_START.stable!; + const altEventTypes = [M_POLL_START.unstable!]; + const room = new Room("room123", null!, null!); + + it("should not add events without a relation", async () => { + // dont pollute console + const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {}); + const relations = new Relations(relationType, eventType, room); + const emitSpy = jest.spyOn(relations, "emit"); + const event = new MatrixEvent({ type: eventType }); + + await relations.addEvent(event); + expect(logSpy).toHaveBeenCalledWith("Event must have relation info"); + // event not added + expect(relations.getRelations().length).toBe(0); + expect(emitSpy).not.toHaveBeenCalled(); + }); + + it("should not add events of incorrect event type", async () => { + // dont pollute console + const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {}); + const relations = new Relations(relationType, eventType, room); + const emitSpy = jest.spyOn(relations, "emit"); + const event = new MatrixEvent({ + type: "different-event-type", + content: { + "m.relates_to": { + event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o", + rel_type: relationType, + }, + }, + }); + + await relations.addEvent(event); + + expect(logSpy).toHaveBeenCalledWith(`Event relation info doesn't match this container`); + // event not added + expect(relations.getRelations().length).toBe(0); + expect(emitSpy).not.toHaveBeenCalled(); + }); + + it("adds events that match alt event types", async () => { + const relations = new Relations(relationType, eventType, room, altEventTypes); + const emitSpy = jest.spyOn(relations, "emit"); + const event = new MatrixEvent({ + type: M_POLL_START.unstable!, + content: { + "m.relates_to": { + event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o", + rel_type: relationType, + }, + }, + }); + + await relations.addEvent(event); + + // event added + expect(relations.getRelations()).toEqual([event]); + expect(emitSpy).toHaveBeenCalledWith(RelationsEvent.Add, event); + }); + + it("should not add events of incorrect relation type", async () => { + const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {}); + const relations = new Relations(relationType, eventType, room); + const event = new MatrixEvent({ + type: eventType, + content: { + "m.relates_to": { + event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o", + rel_type: "m.annotation", + }, + }, + }); + + await relations.addEvent(event); + const emitSpy = jest.spyOn(relations, "emit"); + + expect(logSpy).toHaveBeenCalledWith(`Event relation info doesn't match this container`); + // event not added + expect(relations.getRelations().length).toBe(0); + expect(emitSpy).not.toHaveBeenCalled(); + }); + }); + it("should emit created regardless of ordering", async function () { const targetEvent = new MatrixEvent({ sender: "@bob:example.com", diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 81f6602e784..38fc2cdc42a 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -33,6 +33,7 @@ import { IRelationsRequestOpts, IStateEventWithRoomId, JoinRule, + MatrixClient, MatrixEvent, MatrixEventEvent, PendingEventOrdering, @@ -49,6 +50,7 @@ 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 { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client"; describe("Room", function () { const roomId = "!foo:bar"; @@ -3225,4 +3227,60 @@ describe("Room", function () { expect(room.getBlacklistUnverifiedDevices()).toBe(false); }); }); + + describe("findPredecessorRoomId", () => { + let client: MatrixClient | null = null; + beforeEach(() => { + client = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(), + supportsExperimentalThreads: jest.fn().mockReturnValue(true), + }); + }); + + function roomCreateEvent(newRoomId: string, predecessorRoomId: string | null): MatrixEvent { + const content: { + creator: string; + ["m.federate"]: boolean; + room_version: string; + predecessor: { event_id: string; room_id: string } | undefined; + } = { + "creator": "@daryl:alexandria.example.com", + "predecessor": undefined, + "m.federate": true, + "room_version": "9", + }; + if (predecessorRoomId) { + content.predecessor = { + event_id: "id_of_last_known_event", + room_id: predecessorRoomId, + }; + } + return new MatrixEvent({ + content, + event_id: `create_event_id_pred_${predecessorRoomId}`, + origin_server_ts: 1432735824653, + room_id: newRoomId, + sender: "@daryl:alexandria.example.com", + state_key: "", + type: "m.room.create", + }); + } + + it("Returns null if there is no create event", () => { + const room = new Room("roomid", client!, "@u:example.com"); + expect(room.findPredecessorRoomId()).toBeNull(); + }); + + it("Returns null if the create event has no predecessor", () => { + const room = new Room("roomid", client!, "@u:example.com"); + room.addLiveEvents([roomCreateEvent("roomid", null)]); + expect(room.findPredecessorRoomId()).toBeNull(); + }); + + it("Returns the predecessor ID if one is provided via create event", () => { + const room = new Room("roomid", client!, "@u:example.com"); + room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]); + expect(room.findPredecessorRoomId()).toBe("replacedroomid"); + }); + }); }); diff --git a/spec/unit/rust-crypto.spec.ts b/spec/unit/rust-crypto.spec.ts new file mode 100644 index 00000000000..69dde71239e --- /dev/null +++ b/spec/unit/rust-crypto.spec.ts @@ -0,0 +1,248 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import "fake-indexeddb/auto"; +import { IDBFactory } from "fake-indexeddb"; +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; +import { + KeysBackupRequest, + KeysClaimRequest, + KeysQueryRequest, + KeysUploadRequest, + OlmMachine, + SignatureUploadRequest, +} from "@matrix-org/matrix-sdk-crypto-js"; +import { Mocked } from "jest-mock"; +import MockHttpBackend from "matrix-mock-request"; + +import { RustCrypto } from "../../src/rust-crypto/rust-crypto"; +import { initRustCrypto } from "../../src/rust-crypto"; +import { HttpApiEvent, HttpApiEventHandlerMap, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../src"; +import { TypedEventEmitter } from "../../src/models/typed-event-emitter"; + +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(); +}); + +describe("RustCrypto", () => { + const TEST_USER = "@alice:example.com"; + const TEST_DEVICE_ID = "TEST_DEVICE"; + + describe(".exportRoomKeys", () => { + let rustCrypto: RustCrypto; + + beforeEach(async () => { + const mockHttpApi = {} as MatrixClient["http"]; + rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto; + }); + + it("should return a list", async () => { + const keys = await rustCrypto.exportRoomKeys(); + expect(Array.isArray(keys)).toBeTruthy(); + }); + }); + + describe("to-device messages", () => { + let rustCrypto: RustCrypto; + + beforeEach(async () => { + const mockHttpApi = {} as MatrixClient["http"]; + rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto; + }); + + it("should pass through unencrypted to-device messages", async () => { + const inputs: IToDeviceEvent[] = [ + { content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" }, + ]; + const res = await rustCrypto.preprocessToDeviceMessages(inputs); + expect(res).toEqual(inputs); + }); + + it("should pass through bad encrypted messages", async () => { + const olmMachine: OlmMachine = rustCrypto["olmMachine"]; + const keys = olmMachine.identityKeys; + const inputs: IToDeviceEvent[] = [ + { + type: "m.room.encrypted", + content: { + algorithm: "m.olm.v1.curve25519-aes-sha2", + sender_key: "IlRMeOPX2e0MurIyfWEucYBRVOEEUMrOHqn/8mLqMjA", + ciphertext: { + [keys.curve25519.toBase64()]: { + type: 0, + body: "ajyjlghi", + }, + }, + }, + sender: "@alice:example.com", + }, + ]; + + const res = await rustCrypto.preprocessToDeviceMessages(inputs); + expect(res).toEqual(inputs); + }); + }); + + describe("outgoing requests", () => { + /** the RustCrypto implementation under test */ + let rustCrypto: RustCrypto; + + /** A mock http backend which rustCrypto is connected to */ + let httpBackend: MockHttpBackend; + + /** a mocked-up OlmMachine which rustCrypto is connected to */ + let olmMachine: Mocked; + + /** A list of results to be returned from olmMachine.outgoingRequest. Each call will shift a result off + * the front of the queue, until it is empty. */ + let outgoingRequestQueue: Array>; + + /** wait for a call to olmMachine.markRequestAsSent */ + function awaitCallToMarkAsSent(): Promise { + return new Promise((resolve, _reject) => { + olmMachine.markRequestAsSent.mockImplementationOnce(async () => { + resolve(undefined); + }); + }); + } + + beforeEach(async () => { + httpBackend = new MockHttpBackend(); + + await RustSdkCryptoJs.initAsync(); + + const dummyEventEmitter = new TypedEventEmitter(); + const httpApi = new MatrixHttpApi(dummyEventEmitter, { + baseUrl: "https://example.com", + prefix: "/_matrix", + fetchFn: httpBackend.fetchFn as typeof global.fetch, + onlyData: true, + }); + + // for these tests we use a mock OlmMachine, with an implementation of outgoingRequests that + // returns objects from outgoingRequestQueue + outgoingRequestQueue = []; + olmMachine = { + outgoingRequests: jest.fn().mockImplementation(() => { + return Promise.resolve(outgoingRequestQueue.shift() ?? []); + }), + markRequestAsSent: jest.fn(), + close: jest.fn(), + } as unknown as Mocked; + + rustCrypto = new RustCrypto(olmMachine, httpApi, TEST_USER, TEST_DEVICE_ID); + }); + + it("should poll for outgoing messages", () => { + rustCrypto.onSyncCompleted({}); + expect(olmMachine.outgoingRequests).toHaveBeenCalled(); + }); + + /* simple requests that map directly to the request body */ + const tests: Array<[any, "POST" | "PUT", string]> = [ + [KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"], + [KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"], + [KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"], + [SignatureUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/signatures/upload"], + [KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"], + ]; + + for (const [RequestClass, expectedMethod, expectedPath] of tests) { + it(`should handle ${RequestClass.name}s`, async () => { + const testBody = '{ "foo": "bar" }'; + const outgoingRequest = new RequestClass("1234", testBody); + outgoingRequestQueue.push([outgoingRequest]); + + const testResponse = '{ "result": 1 }'; + httpBackend + .when(expectedMethod, "/_matrix") + .check((req) => { + expect(req.path).toEqual(expectedPath); + expect(req.rawData).toEqual(testBody); + expect(req.headers["Accept"]).toEqual("application/json"); + expect(req.headers["Content-Type"]).toEqual("application/json"); + }) + .respond(200, testResponse, true); + + rustCrypto.onSyncCompleted({}); + + expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1); + + const markSentCallPromise = awaitCallToMarkAsSent(); + await httpBackend.flushAllExpected(); + + await 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 }; + outgoingRequestQueue.push([outgoingRequest]); + + rustCrypto.onSyncCompleted({}); + + await awaitCallToMarkAsSent(); + expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, ""); + }); + + it("stops looping when stop() is called", async () => { + const testResponse = '{ "result": 1 }'; + + for (let i = 0; i < 5; i++) { + outgoingRequestQueue.push([new KeysQueryRequest("1234", "{}")]); + httpBackend.when("POST", "/_matrix").respond(200, testResponse, true); + } + + rustCrypto.onSyncCompleted({}); + + expect(rustCrypto["outgoingRequestLoopRunning"]).toBeTruthy(); + + // go a couple of times round the loop + await httpBackend.flush("/_matrix", 1); + await awaitCallToMarkAsSent(); + + await httpBackend.flush("/_matrix", 1); + await awaitCallToMarkAsSent(); + + // a second sync while this is going on shouldn't make any difference + rustCrypto.onSyncCompleted({}); + + await httpBackend.flush("/_matrix", 1); + await awaitCallToMarkAsSent(); + + // now stop... + rustCrypto.stop(); + + // which should (eventually) cause the loop to stop with no further calls to outgoingRequests + olmMachine.outgoingRequests.mockReset(); + + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + expect(rustCrypto["outgoingRequestLoopRunning"]).toBeFalsy(); + httpBackend.verifyNoOutstandingRequests(); + expect(olmMachine.outgoingRequests).not.toHaveBeenCalled(); + + // we sent three, so there should be 2 left + expect(outgoingRequestQueue.length).toEqual(2); + }); + }); +}); diff --git a/spec/unit/stores/memory.spec.ts b/spec/unit/stores/memory.spec.ts new file mode 100644 index 00000000000..fac3267dbba --- /dev/null +++ b/spec/unit/stores/memory.spec.ts @@ -0,0 +1,65 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent, MemoryStore } from "../../../src"; + +describe("MemoryStore", () => { + const event1 = new MatrixEvent({ type: "event1-type", content: { test: 1 } }); + const event2 = new MatrixEvent({ type: "event2-type", content: { test: 1 } }); + const event3 = new MatrixEvent({ type: "event3-type", content: { test: 1 } }); + const event4 = new MatrixEvent({ type: "event4-type", content: { test: 1 } }); + const event4Updated = new MatrixEvent({ type: "event4-type", content: { test: 2 } }); + const event1Empty = new MatrixEvent({ type: "event1-type", content: {} }); + + describe("account data", () => { + it("sets account data events correctly", () => { + const store = new MemoryStore(); + store.storeAccountDataEvents([event1, event2]); + expect(store.getAccountData(event1.getType())).toEqual(event1); + expect(store.getAccountData(event2.getType())).toEqual(event2); + }); + + it("returns undefined when no account data event exists for type", () => { + const store = new MemoryStore(); + expect(store.getAccountData("my-event-type")).toEqual(undefined); + }); + + it("updates account data events correctly", () => { + const store = new MemoryStore(); + // init store with event1, event2 + store.storeAccountDataEvents([event1, event2, event4]); + // remove event1, add event3 + store.storeAccountDataEvents([event1Empty, event3, event4Updated]); + // removed + expect(store.getAccountData(event1.getType())).toEqual(undefined); + // not removed + expect(store.getAccountData(event2.getType())).toEqual(event2); + // added + expect(store.getAccountData(event3.getType())).toEqual(event3); + // updated + expect(store.getAccountData(event4.getType())).toEqual(event4Updated); + }); + + it("removes all account data from state on deleteAllData", async () => { + const store = new MemoryStore(); + store.storeAccountDataEvents([event1, event2]); + await store.deleteAllData(); + + // empty object + expect(store.accountData).toEqual({}); + }); + }); +}); diff --git a/spec/unit/webrtc/groupCall.spec.ts b/spec/unit/webrtc/groupCall.spec.ts index 59cdbac129d..7d822ec334e 100644 --- a/spec/unit/webrtc/groupCall.spec.ts +++ b/spec/unit/webrtc/groupCall.spec.ts @@ -142,6 +142,15 @@ describe("Group Call", function () { } as unknown as RoomMember; }); + it.each(Object.values(GroupCallState).filter((v) => v !== GroupCallState.LocalCallFeedUninitialized))( + "throws when initializing local call feed in %s state", + async (state: GroupCallState) => { + // @ts-ignore + groupCall.state = state; + await expect(groupCall.initLocalCallFeed()).rejects.toThrowError(); + }, + ); + it("does not initialize local call feed, if it already is", async () => { await groupCall.initLocalCallFeed(); jest.spyOn(groupCall, "initLocalCallFeed"); @@ -308,6 +317,17 @@ describe("Group Call", function () { } }); + describe("hasLocalParticipant()", () => { + it("should return false, if we don't have a local participant", () => { + expect(groupCall.hasLocalParticipant()).toBeFalsy(); + }); + + it("should return true, if we do have local participant", async () => { + await groupCall.enter(); + expect(groupCall.hasLocalParticipant()).toBeTruthy(); + }); + }); + describe("call feeds changing", () => { let call: MockMatrixCall; const currentFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("current")); @@ -475,7 +495,7 @@ describe("Group Call", function () { const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId); // @ts-ignore groupCall.calls.set( - mockCall.getOpponentMember() as RoomMember, + mockCall.getOpponentMember().userId!, new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]), ); @@ -501,7 +521,7 @@ describe("Group Call", function () { const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId); // @ts-ignore groupCall.calls.set( - mockCall.getOpponentMember() as RoomMember, + mockCall.getOpponentMember().userId!, new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]), ); @@ -663,9 +683,7 @@ describe("Group Call", function () { expect(client1.sendToDevice).toHaveBeenCalled(); // @ts-ignore - const oldCall = groupCall1.calls - .get(groupCall1.room.getMember(client2.userId)!)! - .get(client2.deviceId)!; + const oldCall = groupCall1.calls.get(client2.userId)!.get(client2.deviceId)!; oldCall.emit(CallEvent.Hangup, oldCall!); client1.sendToDevice.mockClear(); @@ -685,9 +703,7 @@ describe("Group Call", function () { let newCall: MatrixCall | undefined; while ( // @ts-ignore - (newCall = groupCall1.calls - .get(groupCall1.room.getMember(client2.userId)!) - ?.get(client2.deviceId)) === undefined || + (newCall = groupCall1.calls.get(client2.userId)?.get(client2.deviceId)) === undefined || newCall.peerConn === undefined || newCall.callId == oldCall.callId ) { @@ -730,7 +746,7 @@ describe("Group Call", function () { groupCall1.setLocalVideoMuted(false); // @ts-ignore - const call = groupCall1.calls.get(groupCall1.room.getMember(client2.userId)!)!.get(client2.deviceId)!; + const call = groupCall1.calls.get(client2.userId)!.get(client2.deviceId)!; call.isMicrophoneMuted = jest.fn().mockReturnValue(true); call.setMicrophoneMuted = jest.fn(); call.isLocalVideoMuted = jest.fn().mockReturnValue(true); @@ -794,7 +810,10 @@ describe("Group Call", function () { it("should mute local video when calling setLocalVideoMuted()", async () => { const groupCall = await createAndEnterGroupCall(mockClient, room); - groupCall.localCallFeed!.setAudioVideoMuted = jest.fn(); + jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream"); + jest.spyOn(groupCall, "updateLocalUsermediaStream"); + jest.spyOn(groupCall.localCallFeed!, "setAudioVideoMuted"); + const setAVMutedArray: ((audioMuted: boolean | null, videoMuted: boolean | null) => void)[] = []; const tracksArray: MediaStreamTrack[] = []; const sendMetadataUpdateArray: (() => Promise)[] = []; @@ -808,7 +827,8 @@ describe("Group Call", function () { await groupCall.setLocalVideoMuted(true); groupCall.localCallFeed!.stream.getVideoTracks().forEach((track) => expect(track.enabled).toBe(false)); - expect(groupCall.localCallFeed!.setAudioVideoMuted).toHaveBeenCalledWith(null, true); + expect(mockClient.getMediaHandler().getUserMediaStream).toHaveBeenCalledWith(true, false); + expect(groupCall.updateLocalUsermediaStream).toHaveBeenCalled(); setAVMutedArray.forEach((f) => expect(f).toHaveBeenCalledWith(null, true)); tracksArray.forEach((track) => expect(track.enabled).toBe(false)); sendMetadataUpdateArray.forEach((f) => expect(f).toHaveBeenCalled()); @@ -839,7 +859,7 @@ describe("Group Call", function () { await sleep(10); // @ts-ignore - const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; + const call = groupCall.calls.get(FAKE_USER_ID_2)!.get(FAKE_DEVICE_ID_2)!; call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember); // @ts-ignore Mock call.pushRemoteFeed( @@ -866,7 +886,7 @@ describe("Group Call", function () { await sleep(10); // @ts-ignore - const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; + const call = groupCall.calls.get(FAKE_USER_ID_2).get(FAKE_DEVICE_ID_2)!; call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember); // @ts-ignore Mock call.pushRemoteFeed( @@ -943,9 +963,7 @@ describe("Group Call", function () { expect(mockCall.reject).not.toHaveBeenCalled(); expect(mockCall.answerWithCallFeeds).toHaveBeenCalled(); // @ts-ignore - expect(groupCall.calls).toEqual( - new Map([[groupCall.room.getMember(FAKE_USER_ID_1)!, new Map([[FAKE_DEVICE_ID_1, mockCall]])]]), - ); + expect(groupCall.calls).toEqual(new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_1, mockCall]])]])); }); it("replaces calls if it already has one with the same user", async () => { @@ -960,9 +978,7 @@ describe("Group Call", function () { expect(oldMockCall.hangup).toHaveBeenCalled(); expect(newMockCall.answerWithCallFeeds).toHaveBeenCalled(); // @ts-ignore - expect(groupCall.calls).toEqual( - new Map([[groupCall.room.getMember(FAKE_USER_ID_1)!, new Map([[FAKE_DEVICE_ID_1, newMockCall]])]]), - ); + expect(groupCall.calls).toEqual(new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_1, newMockCall]])]])); }); it("starts to process incoming calls when we've entered", async () => { @@ -975,6 +991,83 @@ describe("Group Call", function () { expect(call.answerWithCallFeeds).toHaveBeenCalled(); }); + + describe("handles call being replaced", () => { + let callChangedListener: jest.Mock; + let oldMockCall: MockMatrixCall; + let newMockCall: MockMatrixCall; + let newCallsMap: Map>; + + beforeEach(() => { + callChangedListener = jest.fn(); + groupCall.addListener(GroupCallEvent.CallsChanged, callChangedListener); + + oldMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId); + newMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId); + newCallsMap = new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_1, newMockCall.typed()]])]]); + + newMockCall.opponentMember = oldMockCall.opponentMember; // Ensure referential equality + newMockCall.callId = "not " + oldMockCall.callId; + mockClient.emit(CallEventHandlerEvent.Incoming, oldMockCall.typed()); + }); + + it("handles regular case", () => { + oldMockCall.emit(CallEvent.Replaced, newMockCall.typed()); + + expect(oldMockCall.hangup).toHaveBeenCalled(); + expect(callChangedListener).toHaveBeenCalledWith(newCallsMap); + // @ts-ignore + expect(groupCall.calls).toEqual(newCallsMap); + }); + + it("handles case where call is missing from the calls map", () => { + // @ts-ignore + groupCall.calls = new Map(); + oldMockCall.emit(CallEvent.Replaced, newMockCall.typed()); + + expect(oldMockCall.hangup).toHaveBeenCalled(); + expect(callChangedListener).toHaveBeenCalledWith(newCallsMap); + // @ts-ignore + expect(groupCall.calls).toEqual(newCallsMap); + }); + }); + + describe("handles call being hangup", () => { + let callChangedListener: jest.Mock; + let mockCall: MockMatrixCall; + + beforeEach(() => { + callChangedListener = jest.fn(); + groupCall.addListener(GroupCallEvent.CallsChanged, callChangedListener); + mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId); + }); + + it("doesn't throw when calls map is empty", () => { + // @ts-ignore + expect(() => groupCall.onCallHangup(mockCall)).not.toThrow(); + }); + + it("clears map completely when we're the last users device left", () => { + mockClient.emit(CallEventHandlerEvent.Incoming, mockCall.typed()); + mockCall.emit(CallEvent.Hangup, mockCall.typed()); + // @ts-ignore + expect(groupCall.calls).toEqual(new Map()); + }); + + it("doesn't remove another call of the same user", () => { + const anotherCallOfTheSameUser = new MockMatrixCall(room.roomId, groupCall.groupCallId); + anotherCallOfTheSameUser.callId = "another call id"; + anotherCallOfTheSameUser.getOpponentDeviceId = () => FAKE_DEVICE_ID_2; + mockClient.emit(CallEventHandlerEvent.Incoming, anotherCallOfTheSameUser.typed()); + + mockClient.emit(CallEventHandlerEvent.Incoming, mockCall.typed()); + mockCall.emit(CallEvent.Hangup, mockCall.typed()); + // @ts-ignore + expect(groupCall.calls).toEqual( + new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_2, anotherCallOfTheSameUser.typed()]])]]), + ); + }); + }); }); describe("screensharing", () => { @@ -1039,7 +1132,7 @@ describe("Group Call", function () { await sleep(10); // @ts-ignore - const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; + const call = groupCall.calls.get(FAKE_USER_ID_2)!.get(FAKE_DEVICE_ID_2)!; call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember); call.onNegotiateReceived({ getContent: () => ({ diff --git a/spec/unit/webrtc/mediaHandler.spec.ts b/spec/unit/webrtc/mediaHandler.spec.ts index 8a595cdc0a9..1dc84e58550 100644 --- a/spec/unit/webrtc/mediaHandler.spec.ts +++ b/spec/unit/webrtc/mediaHandler.spec.ts @@ -308,20 +308,18 @@ describe("Media Handler", function () { expect(stream2.isCloneOf(stream1)).toEqual(false); }); - it("strips unwanted audio tracks from re-used stream", async () => { - const stream1 = await mediaHandler.getUserMediaStream(true, true); - const stream2 = (await mediaHandler.getUserMediaStream(false, true)) as unknown as MockMediaStream; + it("creates new stream when we no longer want audio", async () => { + await mediaHandler.getUserMediaStream(true, true); + const stream = await mediaHandler.getUserMediaStream(false, true); - expect(stream2.isCloneOf(stream1)).toEqual(true); - expect(stream2.getAudioTracks().length).toEqual(0); + expect(stream.getAudioTracks().length).toEqual(0); }); - it("strips unwanted video tracks from re-used stream", async () => { - const stream1 = await mediaHandler.getUserMediaStream(true, true); - const stream2 = (await mediaHandler.getUserMediaStream(true, false)) as unknown as MockMediaStream; + it("creates new stream when we no longer want video", async () => { + await mediaHandler.getUserMediaStream(true, true); + const stream = await mediaHandler.getUserMediaStream(true, false); - expect(stream2.isCloneOf(stream1)).toEqual(true); - expect(stream2.getVideoTracks().length).toEqual(0); + expect(stream.getVideoTracks().length).toEqual(0); }); }); diff --git a/src/@types/crypto.ts b/src/@types/crypto.ts index 91e4d161c7b..0b350a9092f 100644 --- a/src/@types/crypto.ts +++ b/src/@types/crypto.ts @@ -44,3 +44,29 @@ export interface IEventDecryptionResult { claimedEd25519Key?: string; untrusted?: boolean; } + +interface Extensible { + [key: string]: any; +} + +/* eslint-disable camelcase */ + +/** The result of a call to {@link MatrixClient.exportRoomKeys} */ +export interface IMegolmSessionData extends Extensible { + /** Sender's Curve25519 device key */ + sender_key: string; + /** Devices which forwarded this session to us (normally empty). */ + forwarding_curve25519_key_chain: string[]; + /** Other keys the sender claims. */ + sender_claimed_keys: Record; + /** Room this session is used in */ + room_id: string; + /** Unique id for the session */ + session_id: string; + /** Base64'ed key data */ + session_key: string; + algorithm?: string; + untrusted?: boolean; +} + +/* eslint-enable camelcase */ diff --git a/src/@types/event.ts b/src/@types/event.ts index 230cb05039d..2859077218d 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -61,11 +61,12 @@ export enum EventType { KeyVerificationDone = "m.key.verification.done", KeyVerificationKey = "m.key.verification.key", KeyVerificationAccept = "m.key.verification.accept", - // XXX this event is not yet supported by js-sdk + // Not used directly - see READY_TYPE in VerificationRequest. KeyVerificationReady = "m.key.verification.ready", // use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback RoomMessageFeedback = "m.room.message.feedback", Reaction = "m.reaction", + PollStart = "org.matrix.msc3381.poll.start", // Room ephemeral events Typing = "m.typing", @@ -165,6 +166,15 @@ export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix */ export const UNSTABLE_MSC2716_MARKER = new UnstableValue("m.room.marker", "org.matrix.msc2716.marker"); +/** + * Name of the "with_relations" request property for relation based redactions. + * {@link https://github.com/matrix-org/matrix-spec-proposals/pull/3912} + */ +export const MSC3912_RELATION_BASED_REDACTIONS_PROP = new UnstableValue( + "with_relations", + "org.matrix.msc3912.with_relations", +); + /** * Functional members type for declaring a purpose of room members (e.g. helpful bots). * Note that this reference is UNSTABLE and subject to breaking changes, including its diff --git a/src/@types/read_receipts.ts b/src/@types/read_receipts.ts index 34f1c67fad7..3032c5934df 100644 --- a/src/@types/read_receipts.ts +++ b/src/@types/read_receipts.ts @@ -54,3 +54,11 @@ export type Receipts = { [userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair (both nullable) }; }; + +export type CachedReceiptStructure = { + eventId: string; + receiptType: string | ReceiptType; + userId: string; + receipt: Receipt; + synthetic: boolean; +}; diff --git a/src/@types/requests.ts b/src/@types/requests.ts index 75296940a9c..12f4d8eb00a 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -21,7 +21,7 @@ import { IRoomEventFilter } from "../filter"; import { Direction } from "../models/event-timeline"; import { PushRuleAction } from "./PushRules"; import { IRoomEvent } from "../sync-accumulator"; -import { EventType, RoomType } from "./event"; +import { EventType, RelationType, RoomType } from "./event"; // allow camelcase as these are things that go onto the wire /* eslint-disable camelcase */ @@ -47,6 +47,18 @@ export interface IJoinRoomOpts { export interface IRedactOpts { reason?: string; + /** + * Whether events related to the redacted event should be redacted. + * + * If specified, then any events which relate to the event being redacted with + * any of the relationship types listed will also be redacted. + * + * Raises an Error if the server does not support it. + * Check for server-side support before using this param with + * client.canSupport.get(Feature.RelationBasedRedactions). + * {@link https://github.com/matrix-org/matrix-spec-proposals/pull/3912} + */ + with_relations?: Array; } export interface ISendEventResponse { diff --git a/src/ToDeviceMessageQueue.ts b/src/ToDeviceMessageQueue.ts index e78c46ba200..ec5922bb68c 100644 --- a/src/ToDeviceMessageQueue.ts +++ b/src/ToDeviceMessageQueue.ts @@ -16,9 +16,11 @@ limitations under the License. import { ToDeviceMessageId } from "./@types/event"; import { logger } from "./logger"; -import { MatrixError, MatrixClient } from "./matrix"; +import { MatrixClient, ClientEvent } from "./client"; +import { MatrixError } from "./http-api"; import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage"; import { MatrixScheduler } from "./scheduler"; +import { SyncState } from "./sync"; const MAX_BATCH_SIZE = 20; @@ -37,12 +39,14 @@ export class ToDeviceMessageQueue { public start(): void { this.running = true; this.sendQueue(); + this.client.on(ClientEvent.Sync, this.onResumedSync); } public stop(): void { this.running = false; if (this.retryTimeout !== null) clearTimeout(this.retryTimeout); this.retryTimeout = null; + this.client.removeListener(ClientEvent.Sync, this.onResumedSync); } public async queueBatch(batch: ToDeviceBatch): Promise { @@ -132,4 +136,15 @@ export class ToDeviceMessageQueue { await this.client.sendToDevice(batch.eventType, contentMap, batch.txnId); } + + /** + * Listen to sync state changes and automatically resend any pending events + * once syncing is resumed + */ + private onResumedSync = (state: SyncState | null, oldState: SyncState | null): void => { + if (state === SyncState.Syncing && oldState !== SyncState.Syncing) { + logger.info(`Resuming queue after resumed sync`); + this.sendQueue(); + } + }; } diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index aa839550f9e..0457175f389 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -214,9 +214,9 @@ export class AutoDiscovery { // Step 5b: Verify there is an identity server listening on the provided // URL. - const isResponse = await this.fetchWellKnownObject(`${isUrl}/_matrix/identity/api/v1`); + const isResponse = await this.fetchWellKnownObject(`${isUrl}/_matrix/identity/v2`); if (!isResponse?.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) { - logger.error("Invalid /api/v1 response"); + logger.error("Invalid /v2 response"); failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; // Supply the base_url to the caller because they may be ignoring diff --git a/src/client.ts b/src/client.ts index a4aa9ded27c..91cc21be0b0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -18,9 +18,10 @@ limitations under the License. * This is an internal module. See {@link MatrixClient} for the public class. */ -import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk"; +import { Optional } from "matrix-events-sdk"; -import { ISyncStateData, SyncApi, SyncState } from "./sync"; +import type { IMegolmSessionData } from "./@types/crypto"; +import { ISyncStateData, SyncApi, SyncApiOptions, SyncState } from "./sync"; import { EventStatus, IContent, @@ -74,7 +75,6 @@ import { ICryptoCallbacks, IBootstrapCrossSigningOpts, ICheckOwnCrossSigningTrustOpts, - IMegolmSessionData, isCryptoAvailable, VerificationMethod, IRoomKeyRequestBody, @@ -154,6 +154,7 @@ import { UNSTABLE_MSC3088_ENABLED, UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, + MSC3912_RELATION_BASED_REDACTIONS_PROP, } from "./@types/event"; import { IdServerUnbindResult, IImageInfo, Preset, Visibility } from "./@types/partials"; import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper"; @@ -211,6 +212,7 @@ import { LocalNotificationSettings } from "./@types/local_notifications"; import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature"; import { CryptoBackend } from "./common-crypto/CryptoBackend"; +import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants"; export type Store = IStore; @@ -454,17 +456,7 @@ export interface IStartClientOpts { slidingSync?: SlidingSync; } -export interface IStoredClientOpts extends IStartClientOpts { - // Crypto manager - crypto?: Crypto; - /** - * A function which is called - * with a room ID and returns a boolean. It should return 'true' if the SDK can - * SAFELY remove events from this room. It may not be safe to remove events if - * there are other references to the timelines for this room. - */ - canResetEntireTimeline: ResetTimelineCallback; -} +export interface IStoredClientOpts extends IStartClientOpts {} export enum RoomVersionStability { Stable = "stable", @@ -840,6 +832,11 @@ interface ITimestampToEventResponse { event_id: string; origin_server_ts: string; } + +interface IWhoamiResponse { + user_id: string; + device_id?: string; +} /* eslint-enable camelcase */ // We're using this constant for methods overloading and inspect whether a variable @@ -1426,20 +1423,18 @@ export class MatrixClient extends TypedEventEmitter { - if (!this.canResetTimelineCallback) { - return false; - } - return this.canResetTimelineCallback(roomId); - }; + this.clientOpts = opts ?? {}; if (this.clientOpts.slidingSync) { - this.syncApi = new SlidingSyncSdk(this.clientOpts.slidingSync, this, this.clientOpts); + this.syncApi = new SlidingSyncSdk( + this.clientOpts.slidingSync, + this, + this.clientOpts, + this.buildSyncApiOptions(), + ); } else { - this.syncApi = new SyncApi(this, this.clientOpts); + this.syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions()); } + this.syncApi.sync(); if (this.clientOpts.clientWellKnownPollPeriod !== undefined) { @@ -1452,6 +1447,22 @@ export class MatrixClient extends TypedEventEmitter { + if (!this.canResetTimelineCallback) { + return false; + } + return this.canResetTimelineCallback(roomId); + }, + }; + } + /** * High level helper method to stop the client from polling and allow a * clean shutdown. @@ -1657,6 +1668,45 @@ export class MatrixClient extends TypedEventEmitter => { + let indexedDB: IDBFactory; + try { + indexedDB = global.indexedDB; + } catch (e) { + // No indexeddb support + return; + } + for (const dbname of [ + `${RUST_SDK_STORE_PREFIX}::matrix-sdk-crypto`, + `${RUST_SDK_STORE_PREFIX}::matrix-sdk-crypto-meta`, + ]) { + const prom = new Promise((resolve, reject) => { + logger.info(`Removing IndexedDB instance ${dbname}`); + const req = indexedDB.deleteDatabase(dbname); + req.onsuccess = (_): void => { + logger.info(`Removed IndexedDB instance ${dbname}`); + resolve(0); + }; + req.onerror = (e): void => { + // In private browsing, Firefox has a global.indexedDB, but attempts to delete an indexeddb + // (even a non-existent one) fail with "DOMException: A mutation operation was attempted on a + // database that did not allow mutations." + // + // it seems like the only thing we can really do is ignore the error. + logger.warn(`Failed to remove IndexedDB instance ${dbname}:`, e); + resolve(0); + }; + req.onblocked = (e): void => { + logger.info(`cannot yet remove IndexedDB instance ${dbname}`); + }; + }); + await prom; + } + }; + promises.push(deleteRustSdkStore()); + return Promise.all(promises).then(); // .then to fix types } @@ -1672,6 +1722,20 @@ export class MatrixClient extends TypedEventEmitter { + if (this.cryptoBackend) { + logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); + return; + } + + const userId = this.getUserId(); + if (userId === null) { + throw new Error( + `Cannot enable encryption on MatrixClient with unknown userId: ` + + `ensure userId is passed in createClient().`, + ); + } + const deviceId = this.getDeviceId(); + if (deviceId === null) { + throw new Error( + `Cannot enable encryption on MatrixClient with unknown deviceId: ` + + `ensure deviceId is passed in createClient().`, + ); + } + + // importing rust-crypto will download the webassembly, so we delay it until we know it will be + // needed. + const RustCrypto = await import("./rust-crypto"); + this.cryptoBackend = await RustCrypto.initRustCrypto(this.http, userId, deviceId); + } + /** * Is end-to-end crypto enabled for this client. * @returns True if end-to-end is enabled. @@ -2943,10 +3047,10 @@ export class MatrixClient extends TypedEventEmitter { - if (!this.crypto) { + if (!this.cryptoBackend) { return Promise.reject(new Error("End-to-end encryption disabled")); } - return this.crypto.exportRoomKeys(); + return this.cryptoBackend.exportRoomKeys(); } /** @@ -3670,13 +3774,9 @@ export class MatrixClient extends TypedEventEmitter { + const msc3391DeleteAccountDataServerSupport = this.canSupport.get(Feature.AccountDataDeletion); + // if deletion is not supported overwrite with empty content + if (msc3391DeleteAccountDataServerSupport === ServerSupport.Unsupported) { + await this.setAccountData(eventType, {}); + return; + } + const path = utils.encodeUri("/user/$userId/account_data/$type", { + $userId: this.getSafeUserId(), + $type: eventType, + }); + const options = + msc3391DeleteAccountDataServerSupport === ServerSupport.Unstable + ? { prefix: "/_matrix/client/unstable/org.matrix.msc3391" } + : undefined; + return await this.http.authedRequest(Method.Delete, path, undefined, undefined, options); + } + /** * Gets the users that are ignored by this client * @returns The array of users that are ignored (empty if none) @@ -3840,7 +3958,7 @@ export class MatrixClient extends TypedEventEmitter(Method.Post, path, queryString, data); const roomId = res.room_id; - const syncApi = new SyncApi(this, this.clientOpts); + const syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions()); const room = syncApi.createRoom(roomId); if (opts.syncRoom) { // v2 will do this for us @@ -4336,9 +4454,11 @@ export class MatrixClient extends TypedEventEmitter | undefined => { - let newEvent: IPartialEvent | undefined; - - if (content["msgtype"] === MsgType.Text) { - newEvent = MessageEvent.from(content["body"], content["formatted_body"]).serialize(); - } else if (content["msgtype"] === MsgType.Emote) { - newEvent = EmoteEvent.from(content["body"], content["formatted_body"]).serialize(); - } else if (content["msgtype"] === MsgType.Notice) { - newEvent = NoticeEvent.from(content["body"], content["formatted_body"]).serialize(); - } - - if (newEvent && content["m.new_content"] && recurse) { - const newContent = makeContentExtensible(content["m.new_content"], false); - if (newContent) { - newEvent.content["m.new_content"] = newContent.content; - } - } - - if (newEvent) { - // copy over all other fields we don't know about - for (const [k, v] of Object.entries(content)) { - if (!newEvent.content.hasOwnProperty(k)) { - newEvent.content[k] = v; - } - } - } - - return newEvent; - }; - const result = makeContentExtensible(sendContent); - if (result) { - eventType = result.type; - sendContent = result.content; - } + const eventType: string = EventType.RoomMessage; + const sendContent: IContent = content as IContent; return this.sendEvent(roomId, threadId as string | null, eventType, sendContent, txnId); } @@ -6016,7 +6122,7 @@ export class MatrixClient extends TypedEventEmitter { this.peekSync?.stopPeeking(); - this.peekSync = new SyncApi(this, this.clientOpts); + this.peekSync = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions()); return this.peekSync.peek(roomId); } @@ -6523,7 +6629,7 @@ export class MatrixClient extends TypedEventEmitter { - // eslint-disable-line camelcase + public async whoami(): Promise { return this.http.authedRequest(Method.Get, "/account/whoami"); } /** * Find the event_id closest to the given timestamp in the given direction. - * @returns A promise of an object containing the event_id and - * origin_server_ts of the closest event to the timestamp in the given - * direction + * @returns Resolves: A promise of an object containing the event_id and + * origin_server_ts of the closest event to the timestamp in the given direction + * @returns Rejects: when the request fails (module:http-api.MatrixError) */ - public timestampToEvent(roomId: string, timestamp: number, dir: Direction): Promise { + public async timestampToEvent( + roomId: string, + timestamp: number, + dir: Direction, + ): Promise { const path = utils.encodeUri("/rooms/$roomId/timestamp_to_event", { $roomId: roomId, }); + const queryParams = { + ts: timestamp.toString(), + dir: dir, + }; - return this.http.authedRequest( - Method.Get, - path, - { - ts: timestamp.toString(), - dir: dir, - }, - undefined, - { - prefix: "/_matrix/client/unstable/org.matrix.msc3030", - }, - ); + try { + return await this.http.authedRequest(Method.Get, path, queryParams, undefined, { + prefix: ClientPrefix.V1, + }); + } catch (err) { + // Fallback to the prefixed unstable endpoint. Since the stable endpoint is + // new, we should also try the unstable endpoint before giving up. We can + // remove this fallback request in a year (remove after 2023-11-28). + if ( + (err).errcode === "M_UNRECOGNIZED" && + // XXX: The 400 status code check should be removed in the future + // when Synapse is compliant with MSC3743. + ((err).httpStatus === 400 || + // This the correct standard status code for an unsupported + // endpoint according to MSC3743. Not Found and Method Not Allowed + // both indicate that this endpoint+verb combination is + // not supported. + (err).httpStatus === 404 || + (err).httpStatus === 405) + ) { + return await this.http.authedRequest(Method.Get, path, queryParams, undefined, { + prefix: "/_matrix/client/unstable/org.matrix.msc3030", + }); + } + + throw err; + } } } diff --git a/src/common-crypto/CryptoBackend.ts b/src/common-crypto/CryptoBackend.ts index 77748cb5050..a90afdf3a82 100644 --- a/src/common-crypto/CryptoBackend.ts +++ b/src/common-crypto/CryptoBackend.ts @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type { IEventDecryptionResult } from "../@types/crypto"; +import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto"; +import type { IToDeviceEvent } from "../sync-accumulator"; import { MatrixEvent } from "../models/event"; /** * Common interface for the crypto implementations */ -export interface CryptoBackend { +export interface CryptoBackend extends SyncCryptoCallbacks { /** * Global override for whether the client should ever send encrypted * messages to unverified devices. This provides the default for rooms which @@ -60,4 +61,52 @@ export interface CryptoBackend { * Rejects with an error if there is a problem decrypting the event. */ decryptEvent(event: MatrixEvent): Promise; + + /** + * Get a list containing all of the room keys + * + * This should be encrypted before returning it to the user. + * + * @returns a promise which resolves to a list of + * session export objects + */ + exportRoomKeys(): Promise; +} + +/** The methods which crypto implementations should expose to the Sync api */ +export interface SyncCryptoCallbacks { + /** + * Called by the /sync loop whenever there are incoming to-device messages. + * + * The implementation may preprocess the received messages (eg, decrypt them) and return an + * updated list of messages for dispatch to the rest of the system. + * + * Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device + * messages, rather than the results of any decryption attempts. + * + * @param events - the received to-device messages + * @returns A list of preprocessed to-device messages. + */ + preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise; + + /** + * Called by the /sync loop after each /sync response is processed. + * + * Used to complete batch processing, or to initiate background processes + * + * @param syncState - information about the completed sync. + */ + onSyncCompleted(syncState: OnSyncCompletedData): void; +} + +export interface OnSyncCompletedData { + /** + * The 'next_batch' result from /sync, which will become the 'since' token for the next call to /sync. + */ + nextSyncToken?: string; + + /** + * True if we are working our way through a backlog of events after connecting. + */ + catchingUp?: boolean; } diff --git a/src/crypto/OlmDevice.ts b/src/crypto/OlmDevice.ts index 9d342b8cc16..1ade989887e 100644 --- a/src/crypto/OlmDevice.ts +++ b/src/crypto/OlmDevice.ts @@ -21,8 +21,7 @@ import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store"; import * as algorithms from "./algorithms"; import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base"; import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm"; -import { IMegolmSessionData } from "./index"; -import { OlmGroupSessionExtraData } from "../@types/crypto"; +import { IMegolmSessionData, OlmGroupSessionExtraData } from "../@types/crypto"; import { IMessage } from "./algorithms/olm"; // The maximum size of an event is 65K, and we base64 the content, so this is a diff --git a/src/crypto/algorithms/base.ts b/src/crypto/algorithms/base.ts index e5c7a379408..06cb1830323 100644 --- a/src/crypto/algorithms/base.ts +++ b/src/crypto/algorithms/base.ts @@ -18,11 +18,12 @@ limitations under the License. * Internal module. Defines the base classes of the encryption implementations */ +import type { IMegolmSessionData } from "../../@types/crypto"; import { MatrixClient } from "../../client"; import { Room } from "../../models/room"; import { OlmDevice } from "../OlmDevice"; import { IContent, MatrixEvent, RoomMember } from "../../matrix"; -import { Crypto, IEncryptedContent, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from ".."; +import { Crypto, IEncryptedContent, IEventDecryptionResult, IncomingRoomKeyRequest } from ".."; import { DeviceInfo } from "../deviceinfo"; import { IRoomEncryption } from "../RoomList"; diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index ef04e8f4e0b..af9588f91d0 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -20,7 +20,7 @@ limitations under the License. import { v4 as uuidv4 } from "uuid"; -import type { IEventDecryptionResult } from "../../@types/crypto"; +import type { IEventDecryptionResult, IMegolmSessionData } from "../../@types/crypto"; import { logger } from "../../logger"; import * as olmlib from "../olmlib"; import { @@ -39,7 +39,7 @@ import { IOlmSessionResult } from "../olmlib"; import { DeviceInfoMap } from "../DeviceList"; import { IContent, MatrixEvent } from "../../models/event"; import { EventType, MsgType, ToDeviceMessageId } from "../../@types/event"; -import { IMegolmEncryptedContent, IMegolmSessionData, IncomingRoomKeyRequest, IEncryptedContent } from "../index"; +import { IMegolmEncryptedContent, IncomingRoomKeyRequest, IEncryptedContent } from "../index"; import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager"; import { OlmGroupSessionExtraData } from "../../@types/crypto"; import { MatrixError } from "../../http-api"; diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index fe1ae6622af..d71cce99c7d 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -18,6 +18,7 @@ limitations under the License. * Classes for dealing with key backup. */ +import type { IMegolmSessionData } from "../@types/crypto"; import { MatrixClient } from "../client"; import { logger } from "../logger"; import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib"; @@ -36,7 +37,7 @@ import { IKeyBackupSession, } from "./keybackup"; import { UnstableValue } from "../NamespacedValue"; -import { CryptoEvent, IMegolmSessionData } from "./index"; +import { CryptoEvent } from "./index"; import { crypto } from "./crypto"; import { HTTPError, MatrixError } from "../http-api"; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 9349550130d..441d569c4ac 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -20,7 +20,7 @@ limitations under the License. import anotherjson from "another-json"; import { v4 as uuidv4 } from "uuid"; -import type { IEventDecryptionResult } from "../@types/crypto"; +import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto"; import type { PkDecryption, PkSigning } from "@matrix-org/olm"; import { EventType, ToDeviceMessageId } from "../@types/event"; import { TypedReEmitter } from "../ReEmitter"; @@ -85,10 +85,11 @@ import { CryptoStore } from "./store/base"; import { IVerificationChannel } from "./verification/request/Channel"; import { TypedEventEmitter } from "../models/typed-event-emitter"; import { IContent } from "../models/event"; -import { ISyncResponse } from "../sync-accumulator"; +import { ISyncResponse, IToDeviceEvent } from "../sync-accumulator"; import { ISignatures } from "../@types/signed"; import { IMessage } from "./algorithms/olm"; -import { CryptoBackend } from "../common-crypto/CryptoBackend"; +import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; +import { RoomState, RoomStateEvent } from "../models/room-state"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -171,26 +172,6 @@ export interface IRoomKeyRequestBody extends IRoomKey { sender_key: string; } -interface Extensible { - [key: string]: any; -} - -export interface IMegolmSessionData extends Extensible { - // Sender's Curve25519 device key - sender_key: string; - // Devices which forwarded this session to us (normally empty). - forwarding_curve25519_key_chain: string[]; - // Other keys the sender claims. - sender_claimed_keys: Record; - // Room this session is used in - room_id: string; - // Unique id for the session - session_id: string; - // Base64'ed key data - session_key: string; - algorithm?: string; - untrusted?: boolean; -} /* eslint-enable camelcase */ interface IDeviceVerificationUpgrade { @@ -2244,9 +2225,6 @@ export class Crypto extends TypedEventEmitter { + room.off(RoomStateEvent.Update, onState); + if (room.membersLoaded()) { + this.trackRoomDevicesImpl(room).catch((e) => { + logger.error(`Error enabling device tracking in ${roomId}`, e); + }); + } + }; + room.on(RoomStateEvent.Update, onState); } } @@ -2642,6 +2629,8 @@ export class Crypto extends TypedEventEmitter { const room = this.clientStore.getRoom(roomId); @@ -2886,11 +2875,22 @@ export class Crypto extends TypedEventEmitter { if (event.isRedacted()) { + // Try to decrypt the redaction event, to support encrypted + // redaction reasons. If we can't decrypt, just fall back to using + // the original redacted_because. const redactionEvent = new MatrixEvent({ room_id: event.getRoomId(), ...event.getUnsigned().redacted_because, }); - const decryptedEvent = await this.decryptEvent(redactionEvent); + let redactedBecause: IEvent = event.getUnsigned().redacted_because!; + if (redactionEvent.isEncrypted()) { + try { + const decryptedEvent = await this.decryptEvent(redactionEvent); + redactedBecause = decryptedEvent.clearEvent as IEvent; + } catch (e) { + logger.warn("Decryption of redaction failed. Falling back to unencrypted event.", e); + } + } return { clearEvent: { @@ -2898,7 +2898,7 @@ export class Crypto extends TypedEventEmitter { + public async onSyncCompleted(syncData: OnSyncCompletedData): Promise { this.deviceList.setSyncToken(syncData.nextSyncToken ?? null); this.deviceList.saveIfDirty(); @@ -3195,6 +3195,21 @@ export class Crypto extends TypedEventEmitter { + // all we do here is filter out encrypted to-device messages with the wrong algorithm. Decryption + // happens later in decryptEvent, via the EventMapper + return events.filter((toDevice) => { + if ( + toDevice.type === EventType.RoomMessageEncrypted && + !["m.olm.v1.curve25519-aes-sha2"].includes(toDevice.content?.algorithm) + ) { + logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender); + return false; + } + return true; + }); + } + private onToDeviceEvent = (event: MatrixEvent): void => { try { logger.log( @@ -3894,5 +3909,5 @@ class IncomingRoomKeyRequestCancellation { } } -// IEventDecryptionResult is re-exported for backwards compatibility, in case any applications are referencing it. -export type { IEventDecryptionResult } from "../@types/crypto"; +// a number of types are re-exported for backwards compatibility, in case any applications are referencing it. +export type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto"; diff --git a/src/embedded.ts b/src/embedded.ts index 2c1617ecbaf..02dac6db2ce 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -167,9 +167,9 @@ export class RoomWidgetClient extends MatrixClient { // still has some valuable helper methods that we make use of, so we // instantiate it anyways if (opts.slidingSync) { - this.syncApi = new SlidingSyncSdk(opts.slidingSync, this, opts); + this.syncApi = new SlidingSyncSdk(opts.slidingSync, this, opts, this.buildSyncApiOptions()); } else { - this.syncApi = new SyncApi(this, opts); + this.syncApi = new SyncApi(this, opts, this.buildSyncApiOptions()); } this.room = this.syncApi.createRoom(this.roomId); diff --git a/src/event-mapper.ts b/src/event-mapper.ts index 81d3d772a59..87db88d6407 100644 --- a/src/event-mapper.ts +++ b/src/event-mapper.ts @@ -60,6 +60,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event event.setThread(thread); } + // TODO: once we get rid of the old libolm-backed crypto, we can restrict this to room events (rather than + // to-device events), because the rust implementation decrypts to-device messages at a higher level. + // Generally we probably want to use a different eventMapper implementation for to-device events because if (event.isEncrypted()) { if (!preventReEmit) { client.reEmitter.reEmit(event, [MatrixEventEvent.Decrypted]); diff --git a/src/feature.ts b/src/feature.ts index d555a860bd9..9141e811c49 100644 --- a/src/feature.ts +++ b/src/feature.ts @@ -26,6 +26,8 @@ export enum Feature { Thread = "Thread", ThreadUnreadNotifications = "ThreadUnreadNotifications", LoginTokenRequest = "LoginTokenRequest", + RelationBasedRedactions = "RelationBasedRedactions", + AccountDataDeletion = "AccountDataDeletion", } type FeatureSupportCondition = { @@ -45,6 +47,12 @@ const featureSupportResolver: Record = { [Feature.LoginTokenRequest]: { unstablePrefixes: ["org.matrix.msc3882"], }, + [Feature.RelationBasedRedactions]: { + unstablePrefixes: ["org.matrix.msc3912"], + }, + [Feature.AccountDataDeletion]: { + unstablePrefixes: ["org.matrix.msc3391"], + }, }; export async function buildFeatureSupportMap(versions: IServerVersions): Promise> { diff --git a/src/http-api/prefix.ts b/src/http-api/prefix.ts index c350b1dee17..f15b1ac1e77 100644 --- a/src/http-api/prefix.ts +++ b/src/http-api/prefix.ts @@ -34,11 +34,6 @@ export enum ClientPrefix { } export enum IdentityPrefix { - /** - * URI path for v1 of the identity API - * @deprecated Use v2. - */ - V1 = "/_matrix/identity/api/v1", /** * URI path for the v2 identity API */ diff --git a/src/models/event.ts b/src/models/event.ts index 1f1c3cfa366..e4ac9691666 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -828,17 +828,7 @@ export class MatrixEvent extends TypedEventEmittere).name !== "DecryptionError") { - // not a decryption error: log the whole exception as an error - // (and don't bother with a retry) - const re = options.isRetry ? "re" : ""; - // For find results: this can produce "Error decrypting event (id=$ev)" and - // "Error redecrypting event (id=$ev)". - logger.error(`Error ${re}decrypting event (${this.getDetails()})`, e); - this.decryptionPromise = null; - this.retryDecryption = false; - return; - } + const detailedError = e instanceof DecryptionError ? (e).detailedString : String(e); err = e as Error; @@ -858,10 +848,7 @@ export class MatrixEvent extends TypedEventEmittere).detailedString, - ); + logger.log(`Error decrypting event (${this.getDetails()}), but retrying: ${detailedError}`); continue; } @@ -870,9 +857,9 @@ export class MatrixEvent extends TypedEventEmittere).detailedString); + logger.warn(`Error decrypting event (${this.getDetails()}): ${detailedError}`); - res = this.badEncryptedMessage((e).message); + res = this.badEncryptedMessage(String(e)); } // at this point, we've either successfully decrypted the event, or have given up diff --git a/src/models/relations.ts b/src/models/relations.ts index bd6f0093814..069bb0a0c69 100644 --- a/src/models/relations.ts +++ b/src/models/relations.ts @@ -33,6 +33,9 @@ export type EventHandlerMap = { [RelationsEvent.Redaction]: (event: MatrixEvent) => void; }; +const matchesEventType = (eventType: string, targetEventType: string, altTargetEventTypes: string[] = []): boolean => + [targetEventType, ...altTargetEventTypes].includes(eventType); + /** * A container for relation events that supports easy access to common ways of * aggregating such events. Each instance holds events that of a single relation @@ -55,11 +58,13 @@ export class Relations extends TypedEventEmitter return this.oobMemberFlags.status === OobStatus.NotStarted; } + /** + * Check if loading of out-of-band-members has completed + * + * @returns true if the full membership list of this room has been loaded. False if it is not started or is in + * progress. + */ + public outOfBandMembersReady(): boolean { + return this.oobMemberFlags.status === OobStatus.Finished; + } + /** * Mark this room state as waiting for out-of-band members, * ensuring it doesn't ask for them to be requested again diff --git a/src/models/room.ts b/src/models/room.ts index e33d571e65d..e1202c523d1 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1,5 +1,5 @@ /* -Copyright 2015 - 2022 The Matrix.org Foundation C.I.C. +Copyright 2015 - 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. @@ -54,7 +54,13 @@ import { FILTER_RELATED_BY_SENDERS, ThreadFilterType, } from "./thread"; -import { MAIN_ROOM_TIMELINE, Receipt, ReceiptContent, ReceiptType } from "../@types/read_receipts"; +import { + CachedReceiptStructure, + MAIN_ROOM_TIMELINE, + Receipt, + ReceiptContent, + ReceiptType, +} from "../@types/read_receipts"; import { IStateEventWithRoomId } from "../@types/search"; import { RelationsContainer } from "./relations-container"; import { ReadReceipt, synthesizeReceipt } from "./read-receipt"; @@ -302,7 +308,14 @@ export class Room extends ReadReceipt { private txnToEvent: Record = {}; // Pending in-flight requests { string: MatrixEvent } private notificationCounts: NotificationCount = {}; private readonly threadNotifications = new Map(); - public readonly cachedThreadReadReceipts = new Map(); + public readonly cachedThreadReadReceipts = new Map(); + // Useful to know at what point the current user has started using threads in this room + private oldestThreadedReceiptTs = Infinity; + /** + * A record of the latest unthread receipts per user + * This is useful in determining whether a user has read a thread or not + */ + private unthreadedReceipts = new Map(); private readonly timelineSets: EventTimelineSet[]; public readonly threadsTimelineSets: EventTimelineSet[] = []; // any filtered timeline sets we're maintaining for this room @@ -441,9 +454,7 @@ export class Room extends ReadReceipt { }); events.forEach(async (serializedEvent: Partial) => { const event = mapper(serializedEvent); - if (event.getType() === EventType.RoomMessageEncrypted && this.client.isCryptoEnabled()) { - await event.attemptDecryption(this.client.crypto!); - } + await client.decryptEventIfNeeded(event); event.setStatus(EventStatus.NOT_SENT); this.addPendingEvent(event, event.getTxnId()!); }); @@ -503,9 +514,8 @@ export class Room extends ReadReceipt { const decryptionPromises = events .slice(readReceiptTimelineIndex) - .filter((event) => event.shouldAttemptDecryption()) .reverse() - .map((event) => event.attemptDecryption(this.client.crypto!, { isRetry: true })); + .map((event) => this.client.decryptEventIfNeeded(event, { isRetry: true })); await Promise.allSettled(decryptionPromises); } @@ -521,9 +531,9 @@ export class Room extends ReadReceipt { const decryptionPromises = this.getUnfilteredTimelineSet() .getLiveTimeline() .getEvents() - .filter((event) => event.shouldAttemptDecryption()) + .slice(0) // copy before reversing .reverse() - .map((event) => event.attemptDecryption(this.client.crypto!, { isRetry: true })); + .map((event) => this.client.decryptEventIfNeeded(event, { isRetry: true })); await Promise.allSettled(decryptionPromises); } @@ -888,6 +898,20 @@ export class Room extends ReadReceipt { return { memberEvents, fromServer }; } + /** + * Check if loading of out-of-band-members has completed + * + * @returns true if the full membership list of this room has been loaded (including if lazy-loading is disabled). + * False if the load is not started or is in progress. + */ + public membersLoaded(): boolean { + if (!this.opts.lazyLoadMembers) { + return true; + } + + return this.currentState.outOfBandMembersReady(); + } + /** * Preloads the member list in case lazy loading * of memberships is in use. Can be called multiple times, @@ -909,10 +933,6 @@ export class Room extends ReadReceipt { const inMemoryUpdate = this.loadMembers() .then((result) => { this.currentState.setOutOfBandMembers(result.memberEvents); - // now the members are loaded, start to track the e2e devices if needed - if (this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId)) { - this.client.crypto!.trackRoomDevices(this.roomId); - } return result.fromServer; }) .catch((err) => { @@ -1094,6 +1114,9 @@ export class Room extends ReadReceipt { for (const timelineSet of this.timelineSets) { timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined); } + for (const thread of this.threads.values()) { + thread.resetLiveTimeline(backPaginationToken, forwardPaginationToken); + } this.fixUpLegacyTimelineFields(); } @@ -1203,7 +1226,7 @@ export class Room extends ReadReceipt { const event = this.findEventById(eventId); const thread = this.findThreadForEvent(event); if (thread) { - return thread.timelineSet.getLiveTimeline(); + return thread.timelineSet.getTimelineForEvent(eventId); } else { return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId); } @@ -2711,9 +2734,20 @@ export class Room extends ReadReceipt { // when the thread is created this.cachedThreadReadReceipts.set(receipt.thread_id!, [ ...(this.cachedThreadReadReceipts.get(receipt.thread_id!) ?? []), - { event, synthetic }, + { eventId, receiptType, userId, receipt, synthetic }, ]); } + + const me = this.client.getUserId(); + // Track the time of the current user's oldest threaded receipt in the room. + if (userId === me && !receiptForMainTimeline && receipt.ts < this.oldestThreadedReceiptTs) { + this.oldestThreadedReceiptTs = receipt.ts; + } + + // Track each user's unthreaded read receipt. + if (!receipt.thread_id && receipt.ts > (this.unthreadedReceipts.get(userId)?.ts ?? 0)) { + this.unthreadedReceipts.set(userId, receipt); + } }); }); }); @@ -2956,6 +2990,29 @@ export class Room extends ReadReceipt { return this.getType() === RoomType.ElementVideo; } + /** + * @returns the ID of the room that was this room's predecessor, or null if + * this room has no predecessor. + */ + public findPredecessorRoomId(): string | null { + const currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); + if (!currentState) { + return null; + } + + const createEvent = currentState.getStateEvents(EventType.RoomCreate, ""); + if (createEvent) { + const predecessor = createEvent.getContent()["predecessor"]; + if (predecessor) { + const roomId = predecessor["room_id"]; + if (roomId) { + return roomId; + } + } + } + return null; + } + private roomNameGenerator(state: RoomNameState): string { if (this.client.roomNameGenerator) { const name = this.client.roomNameGenerator(this.roomId, state); @@ -3264,6 +3321,26 @@ export class Room extends ReadReceipt { } event.applyVisibilityEvent(visibilityChange); } + + /** + * Find when a client has gained thread capabilities by inspecting the oldest + * threaded receipt + * @returns the timestamp of the oldest threaded receipt + */ + public getOldestThreadedReceiptTs(): number { + return this.oldestThreadedReceiptTs; + } + + /** + * Returns the most recent unthreaded receipt for a given user + * @param userId - the MxID of the User + * @returns an unthreaded Receipt. Can be undefined if receipts have been disabled + * or a user chooses to use private read receipts (or we have simply not received + * a receipt from this user yet). + */ + public getLastUnthreadedReceiptFor(userId: string): Receipt | undefined { + return this.unthreadedReceipts.get(userId); + } } // a map from current event status to a list of allowed next statuses diff --git a/src/models/thread.ts b/src/models/thread.ts index bca2d92b427..31587bbafb7 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -1,5 +1,5 @@ /* -Copyright 2021-2022 The Matrix.org Foundation C.I.C. +Copyright 2021 - 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. @@ -22,12 +22,12 @@ import { RelationType } from "../@types/event"; import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "./event"; import { Direction, EventTimeline } from "./event-timeline"; import { EventTimelineSet, EventTimelineSetHandlerMap } from "./event-timeline-set"; -import { NotificationCountType, Room, RoomEvent } from "./room"; +import { Room, RoomEvent } from "./room"; import { RoomState } from "./room-state"; import { ServerControlledNamespacedValue } from "../NamespacedValue"; import { logger } from "../logger"; import { ReadReceipt } from "./read-receipt"; -import { Receipt, ReceiptContent, ReceiptType } from "../@types/read_receipts"; +import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts"; export enum ThreadEvent { New = "Thread.new", @@ -50,7 +50,7 @@ interface IThreadOpts { room: Room; client: MatrixClient; pendingEventOrdering?: PendingEventOrdering; - receipts?: { event: MatrixEvent; synthetic: boolean }[]; + receipts?: CachedReceiptStructure[]; } export enum FeatureSupport { @@ -97,6 +97,11 @@ export class Thread extends ReadReceipt { private readonly pendingEventOrdering: PendingEventOrdering; public initialEventsFetched = !Thread.hasServerSideSupport; + /** + * An array of events to add to the timeline once the thread has been initialised + * with server suppport. + */ + public replayEvents: MatrixEvent[] | null = []; public constructor(public readonly id: string, public rootEvent: MatrixEvent | undefined, opts: IThreadOpts) { super(); @@ -251,7 +256,7 @@ export class Thread extends ReadReceipt { this.setEventMetadata(event); const lastReply = this.lastReply(); - const isNewestReply = !lastReply || event.localTimestamp > lastReply!.localTimestamp; + const isNewestReply = !lastReply || event.localTimestamp >= lastReply!.localTimestamp; // Add all incoming events to the thread's timeline set when there's no server support if (!Thread.hasServerSideSupport) { @@ -266,6 +271,20 @@ export class Thread extends ReadReceipt { this.addEventToTimeline(event, false); this.fetchEditsWhereNeeded(event); } else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) { + if (!this.initialEventsFetched) { + /** + * A thread can be fully discovered via a single sync response + * And when that's the case we still ask the server to do an initialisation + * as it's the safest to ensure we have everything. + * However when we are in that scenario we might loose annotation or edits + * + * This fix keeps a reference to those events and replay them once the thread + * has been initialised properly. + */ + this.replayEvents?.push(event); + } else { + this.addEventToTimeline(event, toStartOfTimeline); + } // Apply annotations and replace relations to the relations of the timeline only this.timelineSet.relations?.aggregateParentEvent(event); this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet); @@ -298,17 +317,9 @@ export class Thread extends ReadReceipt { * and apply them to the current thread * @param receipts - A collection of the receipts cached from initial sync */ - private processReceipts(receipts: { event: MatrixEvent; synthetic: boolean }[] = []): void { - for (const { event, synthetic } of receipts) { - const content = event.getContent(); - Object.keys(content).forEach((eventId: string) => { - Object.keys(content[eventId]).forEach((receiptType: ReceiptType | string) => { - Object.keys(content[eventId][receiptType]).forEach((userId: string) => { - const receipt = content[eventId][receiptType][userId] as Receipt; - this.addReceiptToStructure(eventId, receiptType as ReceiptType, userId, receipt, synthetic); - }); - }); - }); + private processReceipts(receipts: CachedReceiptStructure[] = []): void { + for (const { eventId, receiptType, userId, receipt, synthetic } of receipts) { + this.addReceiptToStructure(eventId, receiptType as ReceiptType, userId, receipt, synthetic); } } @@ -347,6 +358,63 @@ export class Thread extends ReadReceipt { this.pendingReplyCount = pendingEvents.length; } + /** + * Reset the live timeline of all timelineSets, and start new ones. + * + *

This is used when /sync returns a 'limited' timeline. 'Limited' means that there's a gap between the messages + * /sync returned, and the last known message in our timeline. In such a case, our live timeline isn't live anymore + * and has to be replaced by a new one. To make sure we can continue paginating our timelines correctly, we have to + * set new pagination tokens on the old and the new timeline. + * + * @param backPaginationToken - token for back-paginating the new timeline + * @param forwardPaginationToken - token for forward-paginating the old live timeline, + * if absent or null, all timelines are reset, removing old ones (including the previous live + * timeline which would otherwise be unable to paginate forwards without this token). + * Removing just the old live timeline whilst preserving previous ones is not supported. + */ + public async resetLiveTimeline( + backPaginationToken?: string | null, + forwardPaginationToken?: string | null, + ): Promise { + const oldLive = this.liveTimeline; + this.timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined); + const newLive = this.liveTimeline; + + // FIXME: Remove the following as soon as https://github.com/matrix-org/synapse/issues/14830 is resolved. + // + // The pagination API for thread timelines currently can't handle the type of pagination tokens returned by sync + // + // To make this work anyway, we'll have to transform them into one of the types that the API can handle. + // One option is passing the tokens to /messages, which can handle sync tokens, and returns the right format. + // /messages does not return new tokens on requests with a limit of 0. + // This means our timelines might overlap a slight bit, but that's not an issue, as we deduplicate messages + // anyway. + + let newBackward: string | undefined; + let oldForward: string | undefined; + if (backPaginationToken) { + const res = await this.client.createMessagesRequest(this.roomId, backPaginationToken, 1, Direction.Forward); + newBackward = res.end; + } + if (forwardPaginationToken) { + const res = await this.client.createMessagesRequest( + this.roomId, + forwardPaginationToken, + 1, + Direction.Backward, + ); + oldForward = res.start; + } + // Only replace the token if we don't have paginated away from this position already. This situation doesn't + // occur today, but if the above issue is resolved, we'd have to go down this path. + if (forwardPaginationToken && oldLive.getPaginationToken(Direction.Forward) === forwardPaginationToken) { + oldLive.setPaginationToken(oldForward ?? null, Direction.Forward); + } + if (backPaginationToken && newLive.getPaginationToken(Direction.Backward) === backPaginationToken) { + newLive.setPaginationToken(newBackward ?? null, Direction.Backward); + } + } + private async updateThreadMetadata(): Promise { this.updatePendingReplyCount(); @@ -375,6 +443,10 @@ export class Thread extends ReadReceipt { limit: Math.max(1, this.length), }); } + for (const event of this.replayEvents!) { + this.addEvent(event, false); + } + this.replayEvents = null; // just to make sure that, if we've created a timeline window for this thread before the thread itself // existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly. this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true); @@ -489,17 +561,80 @@ export class Thread extends ReadReceipt { throw new Error("Unsupported function on the thread model"); } + /** + * Get the ID of the event that a given user has read up to within this thread, + * or null if we have received no read receipt (at all) from them. + * @param userId - The user ID to get read receipt event ID for + * @param ignoreSynthesized - If true, return only receipts that have been + * sent by the server, not implicit ones generated + * by the JS SDK. + * @returns ID of the latest event that the given user has read, or null. + */ + public getEventReadUpTo(userId: string, ignoreSynthesized?: boolean): string | null { + const isCurrentUser = userId === this.client.getUserId(); + const lastReply = this.timeline.at(-1); + if (isCurrentUser && lastReply) { + // If the last activity in a thread is prior to the first threaded read receipt + // sent in the room (suggesting that it was sent before the user started + // using a client that supported threaded read receipts), we want to + // consider this thread as read. + const beforeFirstThreadedReceipt = lastReply.getTs() < this.room.getOldestThreadedReceiptTs(); + const lastReplyId = lastReply.getId(); + // Some unsent events do not have an ID, we do not want to consider them read + if (beforeFirstThreadedReceipt && lastReplyId) { + return lastReplyId; + } + } + + const readUpToId = super.getEventReadUpTo(userId, ignoreSynthesized); + + // Check whether the unthreaded read receipt for that user is more recent + // than the read receipt inside that thread. + if (lastReply) { + const unthreadedReceipt = this.room.getLastUnthreadedReceiptFor(userId); + if (!unthreadedReceipt) { + return readUpToId; + } + + for (let i = this.timeline?.length - 1; i >= 0; --i) { + const ev = this.timeline[i]; + // If we encounter the `readUpToId` we do not need to look further + // there is no "more recent" unthreaded read receipt + if (ev.getId() === readUpToId) return readUpToId; + + // Inspecting events from most recent to oldest, we're checking + // whether an unthreaded read receipt is more recent that the current event. + // We usually prefer relying on the order of the DAG but in this scenario + // it is not possible and we have to rely on timestamp + if (ev.getTs() < unthreadedReceipt.ts) return ev.getId() ?? readUpToId; + } + } + + return readUpToId; + } + + /** + * Determine if the given user has read a particular event. + * + * It is invalid to call this method with an event that is not part of this thread. + * + * This is not a definitive check as it only checks the events that have been + * loaded client-side at the time of execution. + * @param userId - The user ID to check the read state of. + * @param eventId - The event ID to check if the user read. + * @returns True if the user has read the event, false otherwise. + */ public hasUserReadEvent(userId: string, eventId: string): boolean { if (userId === this.client.getUserId()) { - const publicReadReceipt = this.getReadReceiptForUserId(userId, false, ReceiptType.Read); - const privateReadReceipt = this.getReadReceiptForUserId(userId, false, ReceiptType.ReadPrivate); - const hasUnreads = this.room.getThreadUnreadNotificationCount(this.id, NotificationCountType.Total) > 0; - - if (!publicReadReceipt && !privateReadReceipt && !hasUnreads) { - // Consider an event read if it's part of a thread that has no - // read receipts and has no notifications. It is likely that it is - // part of a thread that was created before read receipts for threads - // were supported (via MSC3771) + // Consider an event read if it's part of a thread that is before the + // first threaded receipt sent in that room. It is likely that it is + // part of a thread that was created before MSC3771 was implemented. + // Or before the last unthreaded receipt for the logged in user + const beforeFirstThreadedReceipt = + (this.lastReply()?.getTs() ?? 0) < this.room.getOldestThreadedReceiptTs(); + const unthreadedReceiptTs = this.room.getLastUnthreadedReceiptFor(userId)?.ts ?? 0; + const beforeLastUnthreadedReceipt = (this?.lastReply()?.getTs() ?? 0) < unthreadedReceiptTs; + if (beforeFirstThreadedReceipt || beforeLastUnthreadedReceipt) { return true; } } diff --git a/src/rust-crypto/constants.ts b/src/rust-crypto/constants.ts new file mode 100644 index 00000000000..9d72060c379 --- /dev/null +++ b/src/rust-crypto/constants.ts @@ -0,0 +1,18 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** The prefix used on indexeddbs created by rust-crypto */ +export const RUST_SDK_STORE_PREFIX = "matrix-js-sdk"; diff --git a/src/rust-crypto/index.ts b/src/rust-crypto/index.ts new file mode 100644 index 00000000000..4c826078f9f --- /dev/null +++ b/src/rust-crypto/index.ts @@ -0,0 +1,46 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; + +import { RustCrypto } from "./rust-crypto"; +import { logger } from "../logger"; +import { CryptoBackend } from "../common-crypto/CryptoBackend"; +import { RUST_SDK_STORE_PREFIX } from "./constants"; +import { IHttpOpts, MatrixHttpApi } from "../http-api"; + +export async function initRustCrypto( + http: MatrixHttpApi, + userId: string, + deviceId: string, +): Promise { + // initialise the rust matrix-sdk-crypto-js, if it hasn't already been done + await RustSdkCryptoJs.initAsync(); + + // enable tracing in the rust-sdk + new RustSdkCryptoJs.Tracing(RustSdkCryptoJs.LoggerLevel.Debug).turnOn(); + + const u = new RustSdkCryptoJs.UserId(userId); + const d = new RustSdkCryptoJs.DeviceId(deviceId); + logger.info("Init OlmMachine"); + + // 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); + + logger.info("Completed rust crypto-sdk setup"); + return rustCrypto; +} diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts new file mode 100644 index 00000000000..b48de46970b --- /dev/null +++ b/src/rust-crypto/rust-crypto.ts @@ -0,0 +1,201 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; +import { + KeysBackupRequest, + KeysClaimRequest, + KeysQueryRequest, + KeysUploadRequest, + SignatureUploadRequest, +} from "@matrix-org/matrix-sdk-crypto-js"; + +import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto"; +import type { IToDeviceEvent } from "../sync-accumulator"; +import { MatrixEvent } from "../models/event"; +import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; +import { logger } from "../logger"; +import { IHttpOpts, MatrixHttpApi, Method } from "../http-api"; +import { QueryDict } from "../utils"; + +/** + * Common interface for all the request types returned by `OlmMachine.outgoingRequests`. + */ +interface OutgoingRequest { + readonly id: string | undefined; + readonly type: number; +} + +/** + * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. + */ +export class RustCrypto implements CryptoBackend { + public globalBlacklistUnverifiedDevices = false; + public globalErrorOnUnknownDevices = false; + + /** whether {@link stop} has been called */ + private stopped = false; + + /** whether {@link outgoingRequestLoop} is currently running */ + private outgoingRequestLoopRunning = false; + + public constructor( + private readonly olmMachine: RustSdkCryptoJs.OlmMachine, + private readonly http: MatrixHttpApi, + _userId: string, + _deviceId: string, + ) {} + + public stop(): void { + // stop() may be called multiple times, but attempting to close() the OlmMachine twice + // will cause an error. + if (this.stopped) { + return; + } + this.stopped = true; + + // make sure we close() the OlmMachine; doing so means that all the Rust objects will be + // cleaned up; in particular, the indexeddb connections will be closed, which means they + // can then be deleted. + this.olmMachine.close(); + } + + public async decryptEvent(event: MatrixEvent): Promise { + await this.olmMachine.decryptRoomEvent("event", new RustSdkCryptoJs.RoomId("room")); + throw new Error("not implemented"); + } + + public async userHasCrossSigningKeys(): Promise { + // TODO + return false; + } + + public async exportRoomKeys(): Promise { + // TODO + return []; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // SyncCryptoCallbacks implementation + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** called by the sync loop to preprocess incoming to-device messages + * + * @param events - the received to-device messages + * @returns A list of preprocessed to-device messages. + */ + public async preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise { + // send the received to-device messages into receiveSyncChanges. We have no info on device-list changes, + // one-time-keys, or fallback keys, so just pass empty data. + const result = await this.olmMachine.receiveSyncChanges( + JSON.stringify(events), + new RustSdkCryptoJs.DeviceLists(), + new Map(), + new Set(), + ); + + // receiveSyncChanges returns a JSON-encoded list of decrypted to-device messages. + return JSON.parse(result); + } + + /** called by the sync loop after processing each sync. + * + * TODO: figure out something equivalent for sliding sync. + * + * @param syncState - information on the completed sync. + */ + public onSyncCompleted(syncState: OnSyncCompletedData): void { + // Processing the /sync may have produced new outgoing requests which need sending, so kick off the outgoing + // request loop, if it's not already running. + this.outgoingRequestLoop(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // Outgoing requests + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private async outgoingRequestLoop(): Promise { + if (this.outgoingRequestLoopRunning) { + return; + } + this.outgoingRequestLoopRunning = true; + try { + while (!this.stopped) { + const outgoingRequests: Object[] = await this.olmMachine.outgoingRequests(); + if (outgoingRequests.length == 0 || this.stopped) { + // no more messages to send (or we have been told to stop): exit the loop + return; + } + for (const msg of outgoingRequests) { + await this.doOutgoingRequest(msg as OutgoingRequest); + } + } + } catch (e) { + logger.error("Error processing outgoing-message requests from rust crypto-sdk", e); + } finally { + this.outgoingRequestLoopRunning = false; + } + } + + private async doOutgoingRequest(msg: OutgoingRequest): Promise { + let resp: string; + + /* refer https://docs.rs/matrix-sdk-crypto/0.6.0/matrix_sdk_crypto/requests/enum.OutgoingRequests.html + * for the complete list of request types + */ + if (msg instanceof KeysUploadRequest) { + resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body); + } else if (msg instanceof KeysQueryRequest) { + resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body); + } else if (msg instanceof KeysClaimRequest) { + resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body); + } else if (msg instanceof SignatureUploadRequest) { + resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body); + } else if (msg instanceof KeysBackupRequest) { + resp = await this.rawJsonRequest(Method.Put, "/_matrix/client/v3/room_keys/keys", {}, msg.body); + } else { + // TODO: ToDeviceRequest, RoomMessageRequest + logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg)); + resp = ""; + } + + if (msg.id) { + await this.olmMachine.markRequestAsSent(msg.id, msg.type, resp); + } + } + + private async rawJsonRequest(method: Method, path: string, queryParams: QueryDict, body: string): Promise { + const opts = { + // inhibit the JSON stringification and parsing within HttpApi. + json: false, + + // nevertheless, we are sending, and accept, JSON. + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + }, + + // we use the full prefix + prefix: "", + }; + + return await this.http.authedRequest(method, path, queryParams, body, opts); + } +} diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index c6d5f98a518..91ff9d7a75e 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -14,12 +14,20 @@ See the License for the specific language governing permissions and 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 { EventTimeline } from "./models/event-timeline"; -import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client"; -import { ISyncStateData, SyncState, _createAndReEmitRoom } from "./sync"; +import { ClientEvent, IStoredClientOpts, MatrixClient } from "./client"; +import { + ISyncStateData, + SyncState, + _createAndReEmitRoom, + SyncApiOptions, + defaultClientOpts, + defaultSyncApiOpts, +} from "./sync"; import { MatrixEvent } from "./models/event"; import { Crypto } from "./crypto"; import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState, ISyncResponse } from "./sync-accumulator"; @@ -102,6 +110,7 @@ class ExtensionE2EE implements Extension { private nextBatch: string | null = null; - public constructor(private readonly client: MatrixClient) {} + public constructor(private readonly client: MatrixClient, private readonly cryptoCallbacks?: SyncCryptoCallbacks) {} public name(): string { return "to_device"; @@ -142,8 +151,12 @@ class ExtensionToDevice implements Extension { const cancelledKeyVerificationTxns: string[] = []; - data.events - ?.map(this.client.getEventMapper()) + let events = data["events"] || []; + if (events.length > 0 && this.cryptoCallbacks) { + events = await this.cryptoCallbacks.preprocessToDeviceMessages(events); + } + events + .map(this.client.getEventMapper()) .map((toDeviceEvent) => { // map is a cheap inline forEach // We want to flag m.key.verification.start events as cancelled @@ -341,6 +354,8 @@ class ExtensionReceipts implements Extension = {}, + opts?: IStoredClientOpts, + syncOpts?: SyncApiOptions, ) { - this.opts.initialSyncLimit = this.opts.initialSyncLimit ?? 8; - this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false; - this.opts.pollTimeout = this.opts.pollTimeout || 30 * 1000; - this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || PendingEventOrdering.Chronological; - this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true; - - if (!opts.canResetEntireTimeline) { - opts.canResetEntireTimeline = (_roomId: string): boolean => { - return false; - }; - } + this.opts = defaultClientOpts(opts); + this.syncOpts = defaultSyncApiOpts(syncOpts); if (client.getNotifTimelineSet()) { client.reEmitter.reEmit(client.getNotifTimelineSet()!, [RoomEvent.Timeline, RoomEvent.TimelineReset]); @@ -371,13 +378,13 @@ export class SlidingSyncSdk { this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this)); this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this)); const extensions: Extension[] = [ - new ExtensionToDevice(this.client), + new ExtensionToDevice(this.client, this.syncOpts.cryptoCallbacks), new ExtensionAccountData(this.client), new ExtensionTyping(this.client), new ExtensionReceipts(this.client), ]; - if (this.opts.crypto) { - extensions.push(new ExtensionE2EE(this.opts.crypto)); + if (this.syncOpts.crypto) { + extensions.push(new ExtensionE2EE(this.syncOpts.crypto)); } extensions.forEach((ext) => { this.slidingSync.registerExtension(ext); @@ -697,7 +704,7 @@ export class SlidingSyncSdk { if (limited) { room.resetLiveTimeline( roomData.prev_batch, - null, // TODO this.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken, + null, // TODO this.syncOpts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken, ); // We have to assume any gap in any timeline is @@ -729,8 +736,8 @@ export class SlidingSyncSdk { const processRoomEvent = async (e: MatrixEvent): Promise => { client.emit(ClientEvent.Event, e); - if (e.isState() && e.getType() == EventType.RoomEncryption && this.opts.crypto) { - await this.opts.crypto.onCryptoEvent(room, e); + if (e.isState() && e.getType() == EventType.RoomEncryption && this.syncOpts.crypto) { + await this.syncOpts.crypto.onCryptoEvent(room, e); } }; diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index b219dad1159..3a2f6739135 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -397,6 +397,10 @@ export class SlidingSync extends TypedEventEmitter { - this.accountData[event.getType()] = event; + // MSC3391: an event with content of {} should be interpreted as deleted + const isDeleted = !Object.keys(event.getContent()).length; + if (isDeleted) { + delete this.accountData[event.getType()]; + } else { + this.accountData[event.getType()] = event; + } }); } diff --git a/src/sync.ts b/src/sync.ts index 0169ff51186..11fe3cc102b 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -25,6 +25,7 @@ limitations under the License. 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"; @@ -34,7 +35,7 @@ import { EventTimeline } from "./models/event-timeline"; import { PushProcessor } from "./pushprocessor"; import { logger } from "./logger"; import { InvalidStoreError, InvalidStoreState } from "./errors"; -import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client"; +import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering, ResetTimelineCallback } from "./client"; import { IEphemeral, IInvitedRoom, @@ -47,6 +48,7 @@ import { IStrippedState, ISyncResponse, ITimeline, + IToDeviceEvent, } from "./sync-accumulator"; import { MatrixEvent } from "./models/event"; import { MatrixError, Method } from "./http-api"; @@ -59,6 +61,7 @@ import { BeaconEvent } from "./models/beacon"; import { IEventsResponse } from "./@types/requests"; import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; import { Feature, ServerSupport } from "./feature"; +import { Crypto } from "./crypto"; const DEBUG = true; @@ -110,6 +113,31 @@ function debuglog(...params: any[]): void { logger.log(...params); } +/** + * Options passed into the constructor of SyncApi by MatrixClient + */ +export interface SyncApiOptions { + /** + * Crypto manager + * + * @deprecated in favour of cryptoCallbacks + */ + crypto?: Crypto; + + /** + * If crypto is enabled on our client, callbacks into the crypto module + */ + cryptoCallbacks?: SyncCryptoCallbacks; + + /** + * A function which is called + * with a room ID and returns a boolean. It should return 'true' if the SDK can + * SAFELY remove events from this room. It may not be safe to remove events if + * there are other references to the timelines for this room. + */ + canResetEntireTimeline?: ResetTimelineCallback; +} + interface ISyncOptions { filter?: string; hasSyncedBefore?: boolean; @@ -163,7 +191,29 @@ type WrappedRoom = T & { isBrandNewRoom: boolean; }; +/** add default settings to an IStoredClientOpts */ +export function defaultClientOpts(opts?: IStoredClientOpts): IStoredClientOpts { + return { + initialSyncLimit: 8, + resolveInvitesToProfiles: false, + pollTimeout: 30 * 1000, + pendingEventOrdering: PendingEventOrdering.Chronological, + experimentalThreadSupport: false, + ...opts, + }; +} + +export function defaultSyncApiOpts(syncOpts?: SyncApiOptions): SyncApiOptions { + return { + canResetEntireTimeline: (_roomId): boolean => false, + ...syncOpts, + }; +} + export class SyncApi { + private readonly opts: IStoredClientOpts; + private readonly syncOpts: SyncApiOptions; + private _peekRoom: Optional = null; private currentSyncRequest?: Promise; private abortController?: AbortController; @@ -180,21 +230,13 @@ export class SyncApi { /** * Construct an entity which is able to sync with a homeserver. * @param client - The matrix client instance to use. - * @param opts - Config options + * @param opts - client config options + * @param syncOpts - sync-specific options passed by the client * @internal */ - public constructor(private readonly client: MatrixClient, private readonly opts: Partial = {}) { - this.opts.initialSyncLimit = this.opts.initialSyncLimit ?? 8; - this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false; - this.opts.pollTimeout = this.opts.pollTimeout || 30 * 1000; - this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || PendingEventOrdering.Chronological; - this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true; - - if (!opts.canResetEntireTimeline) { - opts.canResetEntireTimeline = (roomId: string): boolean => { - return false; - }; - } + public constructor(private readonly client: MatrixClient, opts?: IStoredClientOpts, syncOpts?: SyncApiOptions) { + this.opts = defaultClientOpts(opts); + this.syncOpts = defaultSyncApiOpts(syncOpts); if (client.getNotifTimelineSet()) { client.reEmitter.reEmit(client.getNotifTimelineSet()!, [RoomEvent.Timeline, RoomEvent.TimelineReset]); @@ -632,7 +674,7 @@ export class SyncApi { return; } if (this.opts.lazyLoadMembers) { - this.opts.crypto?.enableLazyLoading(); + this.syncOpts.crypto?.enableLazyLoading(); } try { debuglog("Storing client options..."); @@ -866,10 +908,10 @@ export class SyncApi { catchingUp: this.catchingUp, }; - if (this.opts.crypto) { + if (this.syncOpts.crypto) { // tell the crypto module we're about to process a sync // response - await this.opts.crypto.onSyncWillProcess(syncEventData); + await this.syncOpts.crypto.onSyncWillProcess(syncEventData); } try { @@ -894,8 +936,8 @@ export class SyncApi { // tell the crypto module to do its processing. It may block (to do a // /keys/changes request). - if (this.opts.crypto) { - await this.opts.crypto.onSyncCompleted(syncEventData); + if (this.syncOpts.cryptoCallbacks) { + await this.syncOpts.cryptoCallbacks.onSyncCompleted(syncEventData); } // keep emitting SYNCING -> SYNCING for clients who want to do bulk updates @@ -907,8 +949,8 @@ export class SyncApi { // stored sync data which means we don't have to worry that we may have missed // device changes. We can also skip the delay since we're not calling this very // frequently (and we don't really want to delay the sync for it). - if (this.opts.crypto) { - await this.opts.crypto.saveDeviceList(0); + if (this.syncOpts.crypto) { + await this.syncOpts.crypto.saveDeviceList(0); } // tell databases that everything is now in a consistent state and can be saved. @@ -1129,19 +1171,15 @@ export class SyncApi { } // handle to-device events - if (Array.isArray(data.to_device?.events) && data.to_device!.events.length > 0) { - const cancelledKeyVerificationTxns: string[] = []; - data.to_device!.events.filter((eventJSON) => { - if ( - eventJSON.type === EventType.RoomMessageEncrypted && - !["m.olm.v1.curve25519-aes-sha2"].includes(eventJSON.content?.algorithm) - ) { - logger.log("Ignoring invalid encrypted to-device event from " + eventJSON.sender); - return false; - } + if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) { + let toDeviceMessages: IToDeviceEvent[] = data.to_device.events; - return true; - }) + if (this.syncOpts.cryptoCallbacks) { + toDeviceMessages = await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages); + } + + const cancelledKeyVerificationTxns: string[] = []; + toDeviceMessages .map(client.getEventMapper({ toDevice: true })) .map((toDeviceEvent) => { // map is a cheap inline forEach @@ -1356,7 +1394,7 @@ export class SyncApi { if (limited) { room.resetLiveTimeline( joinObj.timeline.prev_batch, - this.opts.canResetEntireTimeline!(room.roomId) ? null : syncEventData.oldSyncToken ?? null, + this.syncOpts.canResetEntireTimeline!(room.roomId) ? null : syncEventData.oldSyncToken ?? null, ); // We have to assume any gap in any timeline is @@ -1370,10 +1408,10 @@ export class SyncApi { // avoids a race condition if the application tries to send a message after the // state event is processed, but before crypto is enabled, which then causes the // crypto layer to complain. - if (this.opts.crypto) { + if (this.syncOpts.crypto) { for (const e of stateEvents.concat(events)) { if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") { - await this.opts.crypto.onCryptoEvent(room, e); + await this.syncOpts.crypto.onCryptoEvent(room, e); } } } @@ -1462,8 +1500,8 @@ export class SyncApi { // Handle device list updates if (data.device_lists) { - if (this.opts.crypto) { - await this.opts.crypto.handleDeviceListChanges(syncEventData, data.device_lists); + if (this.syncOpts.crypto) { + await this.syncOpts.crypto.handleDeviceListChanges(syncEventData, data.device_lists); } else { // FIXME if we *don't* have a crypto module, we still need to // invalidate the device lists. But that would require a @@ -1472,12 +1510,12 @@ export class SyncApi { } // Handle one_time_keys_count - if (this.opts.crypto && data.device_one_time_keys_count) { + if (this.syncOpts.crypto && data.device_one_time_keys_count) { const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0; - this.opts.crypto.updateOneTimeKeyCount(currentCount); + this.syncOpts.crypto.updateOneTimeKeyCount(currentCount); } if ( - this.opts.crypto && + this.syncOpts.crypto && (data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"]) ) { // The presence of device_unused_fallback_key_types indicates that the @@ -1485,7 +1523,7 @@ export class SyncApi { // signed_curve25519 fallback key we need a new one. const unusedFallbackKeys = data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"]; - this.opts.crypto.setNeedsNewFallback( + this.syncOpts.crypto.setNeedsNewFallback( Array.isArray(unusedFallbackKeys) && !unusedFallbackKeys.includes("signed_curve25519"), ); } diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index e9276f44fc8..d37df7cda6a 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -1280,7 +1280,10 @@ export class MatrixCall extends TypedEventEmitter t.kind === kind)) { + this.peerConn?.removeTrack(sender); + } + } + // Thirdly, we replace the old tracks, if possible. for (const track of stream.getTracks()) { const tKey = getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, track.kind); - const oldSender = this.transceivers.get(tKey)?.sender; + const transceiver = this.transceivers.get(tKey); + const oldSender = transceiver?.sender; let added = false; if (oldSender) { try { @@ -1306,6 +1322,10 @@ export class MatrixCall extends TypedEventEmitter void; [GroupCallEvent.ActiveSpeakerChanged]: (activeSpeaker: CallFeed | undefined) => void; - [GroupCallEvent.CallsChanged]: (calls: Map>) => void; + [GroupCallEvent.CallsChanged]: (calls: Map>) => void; [GroupCallEvent.UserMediaFeedsChanged]: (feeds: CallFeed[]) => void; [GroupCallEvent.ScreenshareFeedsChanged]: (feeds: CallFeed[]) => void; [GroupCallEvent.LocalScreenshareStateChanged]: ( @@ -197,11 +197,11 @@ export class GroupCall extends TypedEventEmitter< public readonly screenshareFeeds: CallFeed[] = []; public groupCallId: string; - private readonly calls = new Map>(); // RoomMember -> device ID -> MatrixCall - private callHandlers = new Map>(); // User ID -> device ID -> handlers + private readonly calls = new Map>(); // user_id -> device_id -> MatrixCall + private callHandlers = new Map>(); // user_id -> device_id -> ICallHandlers private activeSpeakerLoopInterval?: ReturnType; private retryCallLoopInterval?: ReturnType; - private retryCallCounts: Map> = new Map(); + private retryCallCounts: Map> = new Map(); // user_id -> device_id -> count private reEmitter: ReEmitter; private transmitTimer: ReturnType | null = null; private participantsExpirationTimer: ReturnType | null = null; @@ -609,6 +609,9 @@ export class GroupCall extends TypedEventEmitter< logger.log( `groupCall ${this.groupCallId} setLocalVideoMuted stream ${this.localCallFeed.stream.id} muted ${muted}`, ); + + const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted); + await this.updateLocalUsermediaStream(stream); this.localCallFeed.setAudioVideoMuted(null, muted); setTracksEnabled(this.localCallFeed.stream.getVideoTracks(), !muted); } else { @@ -728,18 +731,18 @@ export class GroupCall extends TypedEventEmitter< return; } - const opponent = newCall.getOpponentMember(); - if (opponent === undefined) { + const opponentUserId = newCall.getOpponentMember()?.userId; + if (opponentUserId === undefined) { logger.warn("Incoming call with no member. Ignoring."); return; } - const deviceMap = this.calls.get(opponent) ?? new Map(); + const deviceMap = this.calls.get(opponentUserId) ?? new Map(); const prevCall = deviceMap.get(newCall.getOpponentDeviceId()!); if (prevCall?.callId === newCall.callId) return; - logger.log(`GroupCall: incoming call from ${opponent.userId} with ID ${newCall.callId}`); + logger.log(`GroupCall: incoming call from ${opponentUserId} with ID ${newCall.callId}`); if (prevCall) this.disposeCall(prevCall, CallErrorCode.Replaced); @@ -747,7 +750,7 @@ export class GroupCall extends TypedEventEmitter< newCall.answerWithCallFeeds(this.getLocalFeeds().map((feed) => feed.clone())); deviceMap.set(newCall.getOpponentDeviceId()!, newCall); - this.calls.set(opponent, deviceMap); + this.calls.set(opponentUserId, deviceMap); this.emit(GroupCallEvent.CallsChanged, this.calls); }; @@ -775,38 +778,38 @@ export class GroupCall extends TypedEventEmitter< private placeOutgoingCalls(): void { let callsChanged = false; - for (const [member, participantMap] of this.participants) { - const callMap = this.calls.get(member) ?? new Map(); + for (const [{ userId }, participantMap] of this.participants) { + const callMap = this.calls.get(userId) ?? new Map(); for (const [deviceId, participant] of participantMap) { const prevCall = callMap.get(deviceId); if ( prevCall?.getOpponentSessionId() !== participant.sessionId && - this.wantsOutgoingCall(member.userId, deviceId) + this.wantsOutgoingCall(userId, deviceId) ) { callsChanged = true; if (prevCall !== undefined) { - logger.debug(`Replacing call ${prevCall.callId} to ${member.userId} ${deviceId}`); + logger.debug(`Replacing call ${prevCall.callId} to ${userId} ${deviceId}`); this.disposeCall(prevCall, CallErrorCode.NewSession); } const newCall = createNewMatrixCall(this.client, this.room.roomId, { - invitee: member.userId, + invitee: userId, opponentDeviceId: deviceId, opponentSessionId: participant.sessionId, groupCallId: this.groupCallId, }); if (newCall === null) { - logger.error(`Failed to create call with ${member.userId} ${deviceId}`); + logger.error(`Failed to create call with ${userId} ${deviceId}`); callMap.delete(deviceId); } else { this.initCall(newCall); callMap.set(deviceId, newCall); - logger.debug(`Placing call to ${member.userId} ${deviceId} (session ${participant.sessionId})`); + logger.debug(`Placing call to ${userId} ${deviceId} (session ${participant.sessionId})`); newCall .placeCallWithCallFeeds( @@ -819,7 +822,7 @@ export class GroupCall extends TypedEventEmitter< } }) .catch((e) => { - logger.warn(`Failed to place call to ${member.userId}`, e); + logger.warn(`Failed to place call to ${userId}`, e); if (e instanceof CallError && e.code === GroupCallErrorCode.UnknownDevice) { this.emit(GroupCallEvent.Error, e); @@ -828,7 +831,7 @@ export class GroupCall extends TypedEventEmitter< GroupCallEvent.Error, new GroupCallError( GroupCallErrorCode.PlaceCallFailed, - `Failed to place call to ${member.userId}`, + `Failed to place call to ${userId}`, ), ); } @@ -841,9 +844,9 @@ export class GroupCall extends TypedEventEmitter< } if (callMap.size > 0) { - this.calls.set(member, callMap); + this.calls.set(userId, callMap); } else { - this.calls.delete(member); + this.calls.delete(userId); } } @@ -865,9 +868,9 @@ export class GroupCall extends TypedEventEmitter< private onRetryCallLoop = (): void => { let needsRetry = false; - for (const [member, participantMap] of this.participants) { - const callMap = this.calls.get(member); - let retriesMap = this.retryCallCounts.get(member); + for (const [{ userId }, participantMap] of this.participants) { + const callMap = this.calls.get(userId); + let retriesMap = this.retryCallCounts.get(userId); for (const [deviceId, participant] of participantMap) { const call = callMap?.get(deviceId); @@ -875,12 +878,12 @@ export class GroupCall extends TypedEventEmitter< if ( call?.getOpponentSessionId() !== participant.sessionId && - this.wantsOutgoingCall(member.userId, deviceId) && + this.wantsOutgoingCall(userId, deviceId) && retries < 3 ) { if (retriesMap === undefined) { retriesMap = new Map(); - this.retryCallCounts.set(member, retriesMap); + this.retryCallCounts.set(userId, retriesMap); } retriesMap.set(deviceId, retries + 1); needsRetry = true; @@ -1020,36 +1023,36 @@ export class GroupCall extends TypedEventEmitter< call.setLocalVideoMuted(videoMuted); } - if (state === CallState.Connected) { - const opponent = call.getOpponentMember()!; - const retriesMap = this.retryCallCounts.get(opponent); + const opponentUserId = call.getOpponentMember()?.userId; + if (state === CallState.Connected && opponentUserId) { + const retriesMap = this.retryCallCounts.get(opponentUserId); retriesMap?.delete(call.getOpponentDeviceId()!); - if (retriesMap?.size === 0) this.retryCallCounts.delete(opponent); + if (retriesMap?.size === 0) this.retryCallCounts.delete(opponentUserId); } }; private onCallHangup = (call: MatrixCall): void => { if (call.hangupReason === CallErrorCode.Replaced) return; - const opponent = call.getOpponentMember() ?? this.room.getMember(call.invitee!)!; - const deviceMap = this.calls.get(opponent); + const opponentUserId = call.getOpponentMember()?.userId ?? this.room.getMember(call.invitee!)!.userId; + const deviceMap = this.calls.get(opponentUserId); // Sanity check that this call is in fact in the map if (deviceMap?.get(call.getOpponentDeviceId()!) === call) { this.disposeCall(call, call.hangupReason as CallErrorCode); deviceMap.delete(call.getOpponentDeviceId()!); - if (deviceMap.size === 0) this.calls.delete(opponent); + if (deviceMap.size === 0) this.calls.delete(opponentUserId); this.emit(GroupCallEvent.CallsChanged, this.calls); } }; private onCallReplaced = (prevCall: MatrixCall, newCall: MatrixCall): void => { - const opponent = prevCall.getOpponentMember()!; + const opponentUserId = prevCall.getOpponentMember()!.userId; - let deviceMap = this.calls.get(opponent); + let deviceMap = this.calls.get(opponentUserId); if (deviceMap === undefined) { deviceMap = new Map(); - this.calls.set(opponent, deviceMap); + this.calls.set(opponentUserId, deviceMap); } this.disposeCall(prevCall, CallErrorCode.Replaced); diff --git a/src/webrtc/mediaHandler.ts b/src/webrtc/mediaHandler.ts index ca311e4a5a3..338701d7189 100644 --- a/src/webrtc/mediaHandler.ts +++ b/src/webrtc/mediaHandler.ts @@ -203,24 +203,30 @@ export class MediaHandler extends TypedEventEmitter< let canReuseStream = true; if (this.localUserMediaStream) { + // This figures out if we can reuse the current localUsermediaStream + // based on whether or not the "mute state" (presence of tracks of a + // given kind) matches what is being requested + if (shouldRequestAudio !== this.localUserMediaStream.getAudioTracks().length > 0) { + canReuseStream = false; + } + if (shouldRequestVideo !== this.localUserMediaStream.getVideoTracks().length > 0) { + canReuseStream = false; + } + // This code checks that the device ID is the same as the localUserMediaStream stream, but we update // the localUserMediaStream whenever the device ID changes (apart from when restoring) so it's not // clear why this would ever be different, unless there's a race. - if (shouldRequestAudio) { - if ( - this.localUserMediaStream.getAudioTracks().length === 0 || - this.localUserMediaStream.getAudioTracks()[0]?.getSettings()?.deviceId !== this.audioInput - ) { - canReuseStream = false; - } + if ( + shouldRequestAudio && + this.localUserMediaStream.getAudioTracks()[0]?.getSettings()?.deviceId !== this.audioInput + ) { + canReuseStream = false; } - if (shouldRequestVideo) { - if ( - this.localUserMediaStream.getVideoTracks().length === 0 || - this.localUserMediaStream.getVideoTracks()[0]?.getSettings()?.deviceId !== this.videoInput - ) { - canReuseStream = false; - } + if ( + shouldRequestVideo && + this.localUserMediaStream.getVideoTracks()[0]?.getSettings()?.deviceId !== this.videoInput + ) { + canReuseStream = false; } } else { canReuseStream = false; diff --git a/yarn.lock b/yarn.lock index c4d494579ad..03a855db843 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,9 +36,9 @@ "@jridgewell/trace-mapping" "^0.3.9" "@babel/cli@^7.12.10": - version "7.19.3" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.19.3.tgz#55914ed388e658e0b924b3a95da1296267e278e2" - integrity sha512-643/TybmaCAe101m2tSVHi9UKpETXP9c/Ff4mD2tAwkdP6esKIfaauZFc67vGEM6r9fekbEGid+sZhbEnSe3dg== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.20.7.tgz#8fc12e85c744a1a617680eacb488fab1fcd35b7c" + integrity sha512-WylgcELHB66WwQqItxNILsMlaTd8/SO6SgTTjMp4uCI7P4QyH1r3nqgFmO3BfM4AtfniHgFMH3EpYFj/zynBkQ== dependencies: "@jridgewell/trace-mapping" "^0.3.8" commander "^4.0.1" @@ -58,31 +58,26 @@ dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.1.tgz#f2e6ef7790d8c8dbf03d379502dcc246dcce0b30" - integrity sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ== - -"@babel/compat-data@^7.20.0": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" - integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5": + version "7.20.10" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.10.tgz#9d92fa81b87542fff50e848ed585b4212c1d34ec" + integrity sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg== "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" - integrity sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.7.tgz#37072f951bd4d28315445f66e0ec9f6ae0c8c35f" + integrity sha512-t1ZjCluspe5DW24bn2Rr1CDb2v9rn/hROtg9a2tmd0+QYf4bsloYfLQzjG4qHPNMhWtKdGC33R5AxGR2Af2cBw== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.0" - "@babel/helper-module-transforms" "^7.20.2" - "@babel/helpers" "^7.20.5" - "@babel/parser" "^7.20.5" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" + "@babel/generator" "^7.20.7" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-module-transforms" "^7.20.7" + "@babel/helpers" "^7.20.7" + "@babel/parser" "^7.20.7" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.7" + "@babel/types" "^7.20.7" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -105,30 +100,12 @@ dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.12.11": - version "7.20.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.3.tgz#e58c9ae2f7bf7fdf4899160cf1e04400a82cd641" - integrity sha512-Wl5ilw2UD1+ZYprHVprxHZJCFeBWlzZYOovE4SDYLZnqCOD11j+0QzNeEWKLLTWM7nixrZEh7vNIyb76MyJg3A== - dependencies: - "@babel/types" "^7.20.2" - "@jridgewell/gen-mapping" "^0.3.2" - jsesc "^2.5.1" - -"@babel/generator@^7.20.1", "@babel/generator@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" - integrity sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA== +"@babel/generator@^7.12.11", "@babel/generator@^7.20.7", "@babel/generator@^7.7.2": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.7.tgz#f8ef57c8242665c5929fe2e8d82ba75460187b4a" + integrity sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw== dependencies: - "@babel/types" "^7.20.5" - "@jridgewell/gen-mapping" "^0.3.2" - jsesc "^2.5.1" - -"@babel/generator@^7.7.2": - version "7.20.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.4.tgz#4d9f8f0c30be75fd90a0562099a26e5839602ab8" - integrity sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA== - dependencies: - "@babel/types" "^7.20.2" + "@babel/types" "^7.20.7" "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" @@ -147,36 +124,37 @@ "@babel/helper-explode-assignable-expression" "^7.18.6" "@babel/types" "^7.18.9" -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz#6bf5374d424e1b3922822f1d9bdaa43b1a139d0a" - integrity sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ== +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" + integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== dependencies: - "@babel/compat-data" "^7.20.0" + "@babel/compat-data" "^7.20.5" "@babel/helper-validator-option" "^7.18.6" 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.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.2.tgz#3c08a5b5417c7f07b5cf3dfb6dc79cbec682e8c2" - integrity sha512-k22GoYRAHPYr9I+Gvy2ZQlAe5mGy8BqWst2wRt8cwIufWTxrsVshhIBvYNqC80N0GSFWTsqRVexOtfzlgOEDvA== +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.20.5", "@babel/helper-create-class-features-plugin@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.7.tgz#d0e1f8d7e4ed5dac0389364d9c0c191d948ade6f" + integrity sha512-LtoWbDXOaidEf50hmdDqn9g8VEzsorMexoWMQdQODbvmqYmaF23pBP5VNPAGIFHsFQCIeKokDiz3CH5Y2jlY6w== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.19.0" - "@babel/helper-member-expression-to-functions" "^7.18.9" + "@babel/helper-member-expression-to-functions" "^7.20.7" "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.19.1" + "@babel/helper-replace-supers" "^7.20.7" "@babel/helper-split-export-declaration" "^7.18.6" -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz#7976aca61c0984202baca73d84e2337a5424a41b" - integrity sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw== +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz#5ea79b59962a09ec2acf20a963a01ab4d076ccca" + integrity sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" - regexpu-core "^5.1.0" + regexpu-core "^5.2.1" "@babel/helper-define-polyfill-provider@^0.3.3": version "0.3.3" @@ -217,12 +195,12 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-member-expression-to-functions@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815" - integrity sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg== +"@babel/helper-member-expression-to-functions@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz#a6f26e919582275a93c3aa6594756d71b0bb7f05" + integrity sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw== dependencies: - "@babel/types" "^7.18.9" + "@babel/types" "^7.20.7" "@babel/helper-module-imports@^7.18.6": version "7.18.6" @@ -231,19 +209,19 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.19.6", "@babel/helper-module-transforms@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712" - integrity sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA== +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.20.7": + version "7.20.11" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0" + integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== dependencies: "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-module-imports" "^7.18.6" "@babel/helper-simple-access" "^7.20.2" "@babel/helper-split-export-declaration" "^7.18.6" "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.2" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.10" + "@babel/types" "^7.20.7" "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" @@ -257,7 +235,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== -"@babel/helper-remap-async-to-generator@^7.18.6", "@babel/helper-remap-async-to-generator@^7.18.9": +"@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== @@ -267,25 +245,26 @@ "@babel/helper-wrap-function" "^7.18.9" "@babel/types" "^7.18.9" -"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz#e1592a9b4b368aa6bdb8784a711e0bcbf0612b78" - integrity sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw== +"@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== dependencies: "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.18.9" + "@babel/helper-member-expression-to-functions" "^7.20.7" "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/traverse" "^7.19.1" - "@babel/types" "^7.19.0" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.7" + "@babel/types" "^7.20.7" -"@babel/helper-simple-access@^7.19.4", "@babel/helper-simple-access@^7.20.2": +"@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== dependencies: "@babel/types" "^7.20.2" -"@babel/helper-skip-transparent-expression-wrappers@^7.18.9": +"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": version "7.20.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== @@ -315,24 +294,24 @@ integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== "@babel/helper-wrap-function@^7.18.9": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz#89f18335cff1152373222f76a4b37799636ae8b1" - integrity sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg== + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3" + integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== dependencies: "@babel/helper-function-name" "^7.19.0" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.19.0" - "@babel/types" "^7.19.0" - -"@babel/helpers@^7.20.5": - version "7.20.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" - integrity sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w== - dependencies: "@babel/template" "^7.18.10" "@babel/traverse" "^7.20.5" "@babel/types" "^7.20.5" +"@babel/helpers@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.7.tgz#04502ff0feecc9f20ecfaad120a18f011a8e6dce" + integrity sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA== + dependencies: + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.7" + "@babel/types" "^7.20.7" + "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -342,15 +321,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": - version "7.20.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2" - integrity sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg== - -"@babel/parser@^7.18.10", "@babel/parser@^7.20.1", "@babel/parser@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" - integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.2.3", "@babel/parser@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.7.tgz#66fe23b3c8569220817d5feb8b9dcdc95bb4f71b" + integrity sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -360,21 +334,21 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz#a11af19aa373d68d561f08e0a57242350ed0ec50" - integrity sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz#d9c85589258539a22a901033853101a6198d4ef1" + integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" - "@babel/plugin-proposal-optional-chaining" "^7.18.9" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/plugin-proposal-optional-chaining" "^7.20.7" "@babel/plugin-proposal-async-generator-functions@^7.20.1": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz#352f02baa5d69f4e7529bdac39aaa02d41146af9" - integrity sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" + integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== dependencies: "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/helper-remap-async-to-generator" "^7.18.9" "@babel/plugin-syntax-async-generators" "^7.8.4" @@ -387,12 +361,12 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-proposal-class-static-block@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz#8aa81d403ab72d3962fc06c26e222dacfc9b9020" - integrity sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.20.7.tgz#92592e9029b13b15be0f7ce6a7aedc2879ca45a7" + integrity sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-create-class-features-plugin" "^7.20.7" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-class-static-block" "^7.14.5" "@babel/plugin-proposal-dynamic-import@^7.18.6": @@ -420,11 +394,11 @@ "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-proposal-logical-assignment-operators@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz#8148cbb350483bf6220af06fa6db3690e14b2e23" - integrity sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz#dfbcaa8f7b4d37b51e8bfb46d94a5aea2bb89d83" + integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": @@ -444,15 +418,15 @@ "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz#a556f59d555f06961df1e572bb5eca864c84022d" - integrity sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" + integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== dependencies: - "@babel/compat-data" "^7.20.1" - "@babel/helper-compilation-targets" "^7.20.0" + "@babel/compat-data" "^7.20.5" + "@babel/helper-compilation-targets" "^7.20.7" "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.20.1" + "@babel/plugin-transform-parameters" "^7.20.7" "@babel/plugin-proposal-optional-catch-binding@^7.18.6": version "7.18.6" @@ -462,13 +436,13 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-proposal-optional-chaining@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz#e8e8fe0723f2563960e4bf5e9690933691915993" - integrity sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w== +"@babel/plugin-proposal-optional-chaining@^7.18.9", "@babel/plugin-proposal-optional-chaining@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz#49f2b372519ab31728cc14115bb0998b15bfda55" + integrity sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-proposal-private-methods@^7.18.6": @@ -480,13 +454,13 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-proposal-private-property-in-object@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz#a64137b232f0aca3733a67eb1a144c192389c503" - integrity sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw== + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz#309c7668f2263f1c711aa399b5a9a6291eef6135" + integrity sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-create-class-features-plugin" "^7.20.5" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": @@ -631,20 +605,20 @@ "@babel/helper-plugin-utils" "^7.19.0" "@babel/plugin-transform-arrow-functions@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz#19063fcf8771ec7b31d742339dac62433d0611fe" - integrity sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ== + 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== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-transform-async-to-generator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz#ccda3d1ab9d5ced5265fdb13f1882d5476c71615" - integrity sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz#dfee18623c8cb31deb796aa3ca84dda9cea94354" + integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q== dependencies: "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-remap-async-to-generator" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-remap-async-to-generator" "^7.18.9" "@babel/plugin-transform-block-scoped-functions@^7.18.6": version "7.18.6" @@ -654,38 +628,39 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-block-scoping@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.2.tgz#f59b1767e6385c663fd0bce655db6ca9c8b236ed" - integrity sha512-y5V15+04ry69OV2wULmwhEA6jwSWXO1TwAtIwiPXcvHcoOQUqpyMVd2bDsQJMW8AurjulIyUV8kDqtjSwHy1uQ== + version "7.20.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.11.tgz#9f5a3424bd112a3f32fe0cf9364fbb155cff262a" + integrity sha512-tA4N427a7fjf1P0/2I4ScsHGc5jcHPbb30xMbaTke2gxDuWpUfXDuX1FEymJwKk4tuGUvGcejAR6HdZVqmmPyw== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-transform-classes@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.2.tgz#c0033cf1916ccf78202d04be4281d161f6709bb2" - integrity sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.7.tgz#f438216f094f6bb31dc266ebfab8ff05aecad073" + integrity sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-compilation-targets" "^7.20.7" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.19.0" "@babel/helper-optimise-call-expression" "^7.18.6" "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-replace-supers" "^7.19.1" + "@babel/helper-replace-supers" "^7.20.7" "@babel/helper-split-export-declaration" "^7.18.6" globals "^11.1.0" "@babel/plugin-transform-computed-properties@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz#2357a8224d402dad623caf6259b611e56aec746e" - integrity sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw== + 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== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/template" "^7.20.7" "@babel/plugin-transform-destructuring@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz#c23741cfa44ddd35f5e53896e88c75331b8b2792" - integrity sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz#8bda578f71620c7de7c93af590154ba331415454" + integrity sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA== dependencies: "@babel/helper-plugin-utils" "^7.20.2" @@ -743,30 +718,30 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-modules-amd@^7.19.6": - version "7.19.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz#aca391801ae55d19c4d8d2ebfeaa33df5f2a2cbd" - integrity sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg== + version "7.20.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz#3daccca8e4cc309f03c3a0c4b41dc4b26f55214a" + integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g== dependencies: - "@babel/helper-module-transforms" "^7.19.6" - "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-transform-modules-commonjs@^7.19.6": - version "7.19.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz#25b32feef24df8038fc1ec56038917eacb0b730c" - integrity sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ== + version "7.20.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.20.11.tgz#8cb23010869bf7669fd4b3098598b6b2be6dc607" + integrity sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw== dependencies: - "@babel/helper-module-transforms" "^7.19.6" - "@babel/helper-plugin-utils" "^7.19.0" - "@babel/helper-simple-access" "^7.19.4" + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-simple-access" "^7.20.2" "@babel/plugin-transform-modules-systemjs@^7.19.6": - version "7.19.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz#59e2a84064b5736a4471b1aa7b13d4431d327e0d" - integrity sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ== + version "7.20.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz#467ec6bba6b6a50634eea61c9c232654d8a4696e" + integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw== dependencies: "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-module-transforms" "^7.19.6" - "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/helper-validator-identifier" "^7.19.1" "@babel/plugin-transform-modules-umd@^7.18.6": @@ -778,12 +753,12 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz#ec7455bab6cd8fb05c525a94876f435a48128888" - integrity sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw== + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" + integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.19.0" - "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-create-regexp-features-plugin" "^7.20.5" + "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-transform-new-target@^7.18.6": version "7.18.6" @@ -800,10 +775,10 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/helper-replace-supers" "^7.18.6" -"@babel/plugin-transform-parameters@^7.20.1": - version "7.20.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.3.tgz#7b3468d70c3c5b62e46be0a47b6045d8590fb748" - integrity sha512-oZg/Fpx0YDrj13KsLyO8I/CX3Zdw7z0O9qOd95SqcoIzuqy/WTGWvePeHAnZCN54SfdyjHcb1S30gc8zlzlHcA== +"@babel/plugin-transform-parameters@^7.20.1", "@babel/plugin-transform-parameters@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz#0ee349e9d1bc96e78e3b37a7af423a4078a7083f" + integrity sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA== dependencies: "@babel/helper-plugin-utils" "^7.20.2" @@ -815,12 +790,12 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-regenerator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz#585c66cb84d4b4bf72519a34cfce761b8676ca73" - integrity sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ== + 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== dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - regenerator-transform "^0.15.0" + "@babel/helper-plugin-utils" "^7.20.2" + regenerator-transform "^0.15.1" "@babel/plugin-transform-reserved-words@^7.18.6": version "7.18.6" @@ -849,12 +824,12 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-spread@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz#dd60b4620c2fec806d60cfaae364ec2188d593b6" - integrity sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz#c2d83e0b99d3bf83e07b11995ee24bf7ca09401e" + integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw== dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" "@babel/plugin-transform-sticky-regex@^7.18.6": version "7.18.6" @@ -878,11 +853,11 @@ "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-transform-typescript@^7.18.6": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.2.tgz#91515527b376fc122ba83b13d70b01af8fe98f3f" - integrity sha512-jvS+ngBfrnTUBfOQq8NfGnSbF9BrqlR6hjJ2yVxMkmO5nL/cdifNbI30EfjRlN4g5wYWNnMPyj5Sa6R1pbLeag== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.7.tgz#673f49499cd810ae32a1ea5f3f8fab370987e055" + integrity sha512-m3wVKEvf6SoszD8pu4NZz3PvfKRCMgk6D6d0Qi9hNnlM5M6CFS92EgF4EiHVLKbU0r/r7ty1hg7NPZwE7WRbYw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.20.2" + "@babel/helper-create-class-features-plugin" "^7.20.7" "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-typescript" "^7.20.0" @@ -1014,66 +989,41 @@ source-map-support "^0.5.16" "@babel/runtime@^7.12.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": - version "7.20.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" - integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" + integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== dependencies: regenerator-runtime "^0.13.11" -"@babel/template@^7.18.10", "@babel/template@^7.3.3": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" - integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== +"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" + integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.18.10" - "@babel/types" "^7.18.10" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" -"@babel/traverse@^7.1.6", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.7.2": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8" - integrity sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA== +"@babel/traverse@^7.1.6", "@babel/traverse@^7.20.10", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.7.2": + version "7.20.10" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.10.tgz#2bf98239597fcec12f842756f186a9dde6d09230" + integrity sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.1" + "@babel/generator" "^7.20.7" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.19.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.1" - "@babel/types" "^7.20.0" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" - integrity sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.5" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.5" - "@babel/types" "^7.20.5" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.2.tgz#67ac09266606190f496322dbaff360fdaa5e7842" - integrity sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog== - dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" - to-fast-properties "^2.0.0" - -"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" - integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg== +"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" + integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== dependencies: "@babel/helper-string-parser" "^7.19.4" "@babel/helper-validator-identifier" "^7.19.1" @@ -1085,9 +1035,9 @@ integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== "@casualbot/jest-sonar-reporter@^2.2.5": - version "2.2.5" - resolved "https://registry.yarnpkg.com/@casualbot/jest-sonar-reporter/-/jest-sonar-reporter-2.2.5.tgz#23d187ddb8d65129a3c8d8239b0540a52c4dc82a" - integrity sha512-Pmb4aEtJudz9G0VsmEUzuDm5iWGOCDsmulzi6AP/RgAXEcmsSxVdxjcgA+2SHC005diU4mXnPLiQyiiMIAtUjA== + version "2.2.7" + resolved "https://registry.yarnpkg.com/@casualbot/jest-sonar-reporter/-/jest-sonar-reporter-2.2.7.tgz#3cc14c64f5d8ab5e9163b03b9cd2e07456432ed0" + integrity sha512-iswhPNodtcOQzfXR3TkD0A/8yHr5fnC86Ryt5UAqrJWfMI8mPZ9mpjykHnibbf91SjNtELv7ApZtha0bdWOmoQ== dependencies: mkdirp "1.0.4" uuid "8.3.2" @@ -1102,7 +1052,7 @@ esquery "^1.4.0" jsdoc-type-pratt-parser "~3.1.0" -"@eslint-community/eslint-utils@^4.1.0": +"@eslint-community/eslint-utils@^4.1.2": version "4.1.2" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.1.2.tgz#14ca568ddaa291dd19a4a54498badc18c6cfab78" integrity sha512-7qELuQWWjVDdVsFQ5+beUl+KPczrEDA7S3zM4QUd/bJl7oXgsmpXaEVqrRTnOBqenOV4rWf2kVZk2Ot085zPWA== @@ -1110,14 +1060,14 @@ eslint-visitor-keys "^3.3.0" "@eslint/eslintrc@^1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95" - integrity sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg== + version "1.4.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" + integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== dependencies: ajv "^6.12.4" debug "^4.3.2" espree "^9.4.0" - globals "^13.15.0" + globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" @@ -1125,9 +1075,9 @@ strip-json-comments "^3.1.1" "@humanwhocodes/config-array@^0.11.6": - version "0.11.7" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f" - integrity sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw== + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" @@ -1426,14 +1376,20 @@ "@jridgewell/sourcemap-codec" "1.4.14" "@jsdoc/salty@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@jsdoc/salty/-/salty-0.2.1.tgz#815c487c859eca81ad3dfea540f830e1ff9d3392" - integrity sha512-JXwylDNSHa549N9uceDYu8D4GMXwSo3H8CCPYEQqxhhHpxD28+lRl2b3bS/caaPj5w1YD3SWtrficJNTnUjGpg== + version "0.2.2" + resolved "https://registry.yarnpkg.com/@jsdoc/salty/-/salty-0.2.2.tgz#567017ddda2048c5ff921aeffd38564a0578fdca" + integrity sha512-A1FrVnc7L9qI2gUGsfN0trTiJNK72Y0CL/VAyrmYEmeKI3pnHDawP64CEev31XLyAAOx2xmDo3tbadPxC0CSbw== dependencies: lodash "^4.17.21" +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.2": + version "0.1.0-alpha.2" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6" + integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA== + "@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": @@ -1603,9 +1559,9 @@ integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== "@sinonjs/commons@^1.7.0": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.5.tgz#e280c94c95f206dcfd5aca00a43f2156b758c764" - integrity sha512-rTpCA0wG1wUxglBSFdMMY0oTrKYvgf4fNgv/sXbfCVAdf+FnPBdKJR/7XbpTCwbCrvCbdPYnlWaUUYz4V2fPDA== + version "1.8.6" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" + integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ== dependencies: type-detect "4.0.8" @@ -1653,9 +1609,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.18.2" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.2.tgz#235bf339d17185bdec25e024ca19cce257cc7309" - integrity sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg== + version "7.18.3" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d" + integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== dependencies: "@babel/types" "^7.3.0" @@ -1717,9 +1673,9 @@ "@types/istanbul-lib-report" "*" "@types/jest@^29.0.0": - version "29.2.3" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.3.tgz#f5fd88e43e5a9e4221ca361e23790d48fcf0a211" - integrity sha512-6XwoEbmatfyoCjWRX7z0fKMmgYKe9+/HrviJ5k0X/tjJWHGAezZOfYaxqQKuzG/TvQyr+ktjm4jgbk0s4/oF2w== + version "29.2.5" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.5.tgz#c27f41a9d6253f288d1910d3c5f09484a56b73c0" + integrity sha512-H2cSxkKgVmqNHXP7TC2L/WUorrZu8ZigyRywfVzv6EyBlxj39n4C00hjXYQWsbwqgElaj/CiAeSRmk5GoaKTgw== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -1744,9 +1700,9 @@ integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/node@*", "@types/node@18": - version "18.11.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" - integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== + version "18.11.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" + integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -1754,9 +1710,9 @@ integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== "@types/prettier@^2.1.5": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e" - integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow== + version "2.7.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" + integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg== "@types/retry@0.12.0": version "0.12.0" @@ -1799,20 +1755,20 @@ integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^17.0.8": - version "17.0.13" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76" - integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg== + version "17.0.19" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.19.tgz#8dbecdc9ab48bee0cb74f6e3327de3fa0d0c98ae" + integrity sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ== dependencies: "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.0.tgz#ffa505cf961d4844d38cfa19dcec4973a6039e41" - integrity sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA== + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz#54f8368d080eb384a455f60c2ee044e948a8ce67" + integrity sha512-SVLafp0NXpoJY7ut6VFVUU9I+YeFsDzeQwtK0WZ+xbRN3mtxJ08je+6Oi2N89qDn087COdO0u3blKZNv9VetRQ== dependencies: - "@typescript-eslint/scope-manager" "5.45.0" - "@typescript-eslint/type-utils" "5.45.0" - "@typescript-eslint/utils" "5.45.0" + "@typescript-eslint/scope-manager" "5.48.0" + "@typescript-eslint/type-utils" "5.48.0" + "@typescript-eslint/utils" "5.48.0" debug "^4.3.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" @@ -1821,71 +1777,71 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.45.0.tgz#b18a5f6b3cf1c2b3e399e9d2df4be40d6b0ddd0e" - integrity sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ== + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.0.tgz#02803355b23884a83e543755349809a50b7ed9ba" + integrity sha512-1mxNA8qfgxX8kBvRDIHEzrRGrKHQfQlbW6iHyfHYS0Q4X1af+S6mkLNtgCOsGVl8+/LUPrqdHMssAemkrQ01qg== dependencies: - "@typescript-eslint/scope-manager" "5.45.0" - "@typescript-eslint/types" "5.45.0" - "@typescript-eslint/typescript-estree" "5.45.0" + "@typescript-eslint/scope-manager" "5.48.0" + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/typescript-estree" "5.48.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz#7a4ac1bfa9544bff3f620ab85947945938319a96" - integrity sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw== +"@typescript-eslint/scope-manager@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.48.0.tgz#607731cb0957fbc52fd754fd79507d1b6659cecf" + integrity sha512-0AA4LviDtVtZqlyUQnZMVHydDATpD9SAX/RC5qh6cBd3xmyWvmXYF+WT1oOmxkeMnWDlUVTwdODeucUnjz3gow== dependencies: - "@typescript-eslint/types" "5.45.0" - "@typescript-eslint/visitor-keys" "5.45.0" + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/visitor-keys" "5.48.0" -"@typescript-eslint/type-utils@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.45.0.tgz#aefbc954c40878fcebeabfb77d20d84a3da3a8b2" - integrity sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q== +"@typescript-eslint/type-utils@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.48.0.tgz#40496dccfdc2daa14a565f8be80ad1ae3882d6d6" + integrity sha512-vbtPO5sJyFjtHkGlGK4Sthmta0Bbls4Onv0bEqOGm7hP9h8UpRsHJwsrCiWtCUndTRNQO/qe6Ijz9rnT/DB+7g== dependencies: - "@typescript-eslint/typescript-estree" "5.45.0" - "@typescript-eslint/utils" "5.45.0" + "@typescript-eslint/typescript-estree" "5.48.0" + "@typescript-eslint/utils" "5.48.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5" - integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA== +"@typescript-eslint/types@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.48.0.tgz#d725da8dfcff320aab2ac6f65c97b0df30058449" + integrity sha512-UTe67B0Ypius0fnEE518NB2N8gGutIlTojeTg4nt0GQvikReVkurqxd2LvYa9q9M5MQ6rtpNyWTBxdscw40Xhw== -"@typescript-eslint/typescript-estree@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d" - integrity sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ== +"@typescript-eslint/typescript-estree@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.0.tgz#a7f04bccb001003405bb5452d43953a382c2fac2" + integrity sha512-7pjd94vvIjI1zTz6aq/5wwE/YrfIyEPLtGJmRfyNR9NYIW+rOvzzUv3Cmq2hRKpvt6e9vpvPUQ7puzX7VSmsEw== dependencies: - "@typescript-eslint/types" "5.45.0" - "@typescript-eslint/visitor-keys" "5.45.0" + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/visitor-keys" "5.48.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.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.45.0.tgz#9cca2996eee1b8615485a6918a5c763629c7acf5" - integrity sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA== +"@typescript-eslint/utils@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.48.0.tgz#eee926af2733f7156ad8d15e51791e42ce300273" + integrity sha512-x2jrMcPaMfsHRRIkL+x96++xdzvrdBCnYRd5QiW5Wgo1OB4kDYPbC1XjWP/TNqlfK93K/lUL92erq5zPLgFScQ== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.45.0" - "@typescript-eslint/types" "5.45.0" - "@typescript-eslint/typescript-estree" "5.45.0" + "@typescript-eslint/scope-manager" "5.48.0" + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/typescript-estree" "5.48.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528" - integrity sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg== +"@typescript-eslint/visitor-keys@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.0.tgz#4446d5e7f6cadde7140390c0e284c8702d944904" + integrity sha512-5motVPz5EgxQ0bHjut3chzBkJ3Z3sheYVcSwS5BpHZpLqSptSmELNtGixmgj65+rIfhvtQTz5i9OP2vtzdDH7Q== dependencies: - "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/types" "5.48.0" eslint-visitor-keys "^3.3.0" JSONStream@^1.0.3: @@ -1902,9 +1858,9 @@ abab@^2.0.6: integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== ace-builds@^1.4.13: - version "1.12.5" - resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.12.5.tgz#e037069148507b4b32c21f56b52a830e9a0759d3" - integrity sha512-2OTOZZdXVqWHfsV63n/bWLJ4uGnGNm9uwEQSECbEzMpKF2RKxD04lWu7s+eRBTZoEbqPXziyI1qamJH2OAwdwA== + version "1.14.0" + resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.14.0.tgz#85a6733b4fa17b0abc3dbfe38cd8d823cad79716" + integrity sha512-3q8LvawomApRCt4cC0OzxVjDsZ609lDbm8l0Xl9uqG06dKEq4RT0YXLUyk7J2SxmqIp5YXzZNw767Dr8GKUruw== acorn-globals@^3.0.0: version "3.1.0" @@ -1960,7 +1916,7 @@ acorn@^7.0.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.1.0, acorn@^8.5.0, acorn@^8.8.0: +acorn@^8.1.0, acorn@^8.5.0, acorn@^8.8.0, acorn@^8.8.1: version "8.8.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== @@ -2047,9 +2003,9 @@ any-promise@^1.3.0: integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== anymatch@^3.0.3, anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -2580,9 +2536,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001400: - version "1.0.30001435" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001435.tgz#502c93dbd2f493bee73a408fe98e98fb1dad10b2" - integrity sha512-kdCkUTjR+v4YAJelyiDTqiu82BDr4W4CP5sgTA0ZBmqn30XfS2ZghPLMowik9TPhS+psWJiUNxsqLyurDbmutA== + version "1.0.30001441" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz#987437b266260b640a23cd18fbddb509d7f69f3e" + integrity sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg== center-align@^0.1.1: version "0.1.3" @@ -2636,15 +2592,10 @@ chokidar@^3.4.0: optionalDependencies: fsevents "~2.3.2" -ci-info@^3.2.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.6.1.tgz#7594f1c95cb7fdfddee7af95a13af7dbc67afdcf" - integrity sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w== - -ci-info@^3.6.1: - version "3.7.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef" - integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog== +ci-info@^3.2.0, ci-info@^3.6.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" + integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" @@ -2847,9 +2798,9 @@ convert-source-map@~1.1.0: integrity sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg== core-js-compat@^3.25.1: - version "3.26.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.26.0.tgz#94e2cf8ba3e63800c4956ea298a6473bc9d62b44" - integrity sha512-piOX9Go+Z4f9ZiBFLnZ5VrOpBl0h7IGCkiFUN11QTe6LjAvOT3ifL/5TdoizMh99hcGy5SoLyWbapIY/PIb/3A== + version "3.27.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.27.1.tgz#b5695eb25c602d72b1d30cbfba3cb7e5e4cf0a67" + integrity sha512-Dg91JFeCDA17FKnneN7oCMz4BkQ4TcffkgHP4OWwp9yx3pi7ubqMDXXSacfNak1PQqjc95skyt+YBLHQJnkJwA== dependencies: browserslist "^4.21.4" @@ -2990,10 +2941,10 @@ decamelize@^1.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== -decimal.js@^10.4.1: - version "10.4.2" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.2.tgz#0341651d1d997d86065a2ce3a441fbd0d8e8b98e" - integrity sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA== +decimal.js@^10.4.2: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== dedent@^0.7.0: version "0.7.0" @@ -3182,9 +3133,9 @@ emoji-regex@^8.0.0: integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== enhanced-resolve@^5.10.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6" - integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ== + version "5.12.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" + integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -3202,9 +3153,9 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.19.0, es-abstract@^1.20.4: - version "1.20.4" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.4.tgz#1d103f9f8d78d4cf0713edcd6d0ed1a46eed5861" - integrity sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA== + version "1.20.5" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.5.tgz#e6dc99177be37cacda5988e692c3fa8b218e95d2" + integrity sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" @@ -3212,6 +3163,7 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: function.prototype.name "^1.1.5" get-intrinsic "^1.1.3" get-symbol-description "^1.0.0" + gopd "^1.0.1" has "^1.0.3" has-property-descriptors "^1.0.0" has-symbols "^1.0.3" @@ -3227,8 +3179,8 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: object.assign "^4.1.4" regexp.prototype.flags "^1.4.3" safe-regex-test "^1.0.0" - string.prototype.trimend "^1.0.5" - string.prototype.trimstart "^1.0.5" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" unbox-primitive "^1.0.2" es-shim-unscopables@^1.0.0: @@ -3321,9 +3273,9 @@ eslint-config-google@^0.14.0: integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== eslint-config-prettier@^8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" - integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== + version "8.6.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz#dec1d29ab728f4fa63061774e1672ac4e363d207" + integrity sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA== eslint-import-resolver-node@^0.3.6: version "0.3.6" @@ -3399,12 +3351,12 @@ eslint-plugin-tsdoc@^0.2.17: "@microsoft/tsdoc-config" "0.16.2" eslint-plugin-unicorn@^45.0.0: - version "45.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-45.0.1.tgz#2307f4620502fd955c819733ce1276bed705b736" - integrity sha512-tLnIw5oDJJc3ILYtlKtqOxPP64FZLTkZkgeuoN6e7x6zw+rhBjOxyvq2c7577LGxXuIhBYrwisZuKNqOOHp3BA== + version "45.0.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-45.0.2.tgz#d6ba704793a6909fe5dfe013900d2b05b715284c" + integrity sha512-Y0WUDXRyGDMcKLiwgL3zSMpHrXI00xmdyixEGIg90gHnj0PcHY4moNv3Ppje/kDivdAy5vUeUr7z211ImPv2gw== dependencies: "@babel/helper-validator-identifier" "^7.19.1" - "@eslint-community/eslint-utils" "^4.1.0" + "@eslint-community/eslint-utils" "^4.1.2" ci-info "^3.6.1" clean-regexp "^1.0.0" esquery "^1.4.0" @@ -3458,10 +3410,10 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.28.0: - version "8.28.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.28.0.tgz#81a680732634677cc890134bcdd9fdfea8e63d6e" - integrity sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ== +eslint@8.29.0: + version "8.29.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.29.0.tgz#d74a88a20fb44d59c51851625bc4ee8d0ec43f87" + integrity sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg== dependencies: "@eslint/eslintrc" "^1.3.3" "@humanwhocodes/config-array" "^0.11.6" @@ -3674,9 +3626,9 @@ fast-safe-stringify@^2.0.7: integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== dependencies: reusify "^1.0.4" @@ -3820,7 +3772,7 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== @@ -3848,9 +3800,9 @@ get-symbol-description@^1.0.0: get-intrinsic "^1.1.1" get-tsconfig@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.2.0.tgz#ff368dd7104dab47bf923404eb93838245c66543" - integrity sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg== + version "4.3.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.3.0.tgz#4c26fae115d1050e836aea65d6fe56b507ee249b" + integrity sha512-YCcF28IqSay3fqpIu5y3Krg/utCBHBeoflkZyHj/QcqI2nrLPC3ZegS9CmIo+hJb8K7aiGsuUl7PwWVjNG2HQQ== glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" @@ -3883,10 +3835,10 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.15.0: - version "13.17.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4" - integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw== +globals@^13.15.0, globals@^13.19.0: + version "13.19.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8" + integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ== dependencies: type-fest "^0.20.2" @@ -3908,9 +3860,9 @@ globby@^11.1.0: slash "^3.0.0" globby@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515" - integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ== + version "13.1.3" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.3.tgz#f62baf5720bcb2c1330c8d4ef222ee12318563ff" + integrity sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw== dependencies: dir-glob "^3.0.1" fast-glob "^3.2.11" @@ -4079,9 +4031,9 @@ ieee754@^1.1.4: integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" - integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" @@ -4151,11 +4103,11 @@ insert-module-globals@^7.2.1: xtend "^4.0.0" internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" + integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== dependencies: - get-intrinsic "^1.1.0" + get-intrinsic "^1.1.3" has "^1.0.3" side-channel "^1.0.4" @@ -4613,9 +4565,9 @@ jest-leak-detector@^29.3.1: pretty-format "^29.3.1" jest-localstorage-mock@^2.4.6: - version "2.4.22" - resolved "https://registry.yarnpkg.com/jest-localstorage-mock/-/jest-localstorage-mock-2.4.22.tgz#9d70be92bfc591c0be289ee2f71de1b4b2a5ca9b" - integrity sha512-60PWSDFQOS5v7JzSmYLM3dPLg0JLl+2Vc4lIEz/rj2yrXJzegsFLn7anwc5IL0WzJbBa/Las064CHbFg491/DQ== + version "2.4.25" + resolved "https://registry.yarnpkg.com/jest-localstorage-mock/-/jest-localstorage-mock-2.4.25.tgz#9be525ebcd4eb791a445dbeba8474ceb2abeb434" + integrity sha512-VdQ8PTpNzUJDx/KY3hBrTwxqVMzMS+LccngC15EZSFdxJ+VeeCYmyW7BSzubk9FUKCVeXPjYPibzXe6swXYA+g== jest-matcher-utils@^28.1.3: version "28.1.3" @@ -4870,9 +4822,9 @@ jju@~1.4.0: integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== js-sdsl@^4.1.4: - version "4.1.5" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.5.tgz#1ff1645e6b4d1b028cd3f862db88c9d887f26e2a" - integrity sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q== + version "4.2.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" + integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== js-stringify@^1.0.1: version "1.0.2" @@ -4905,17 +4857,17 @@ jsdoc-type-pratt-parser@~3.1.0: integrity sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw== jsdom@^20.0.0: - version "20.0.2" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.2.tgz#65ccbed81d5e877c433f353c58bb91ff374127db" - integrity sha512-AHWa+QO/cgRg4N+DsmHg1Y7xnz+8KU3EflM0LVDTdmrYOc1WWTSkOjtpUveQH+1Bqd5rtcVnb/DuxV/UjDO4rA== + version "20.0.3" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db" + integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== dependencies: abab "^2.0.6" - acorn "^8.8.0" + acorn "^8.8.1" acorn-globals "^7.0.0" cssom "^0.5.0" cssstyle "^2.3.0" data-urls "^3.0.2" - decimal.js "^10.4.1" + decimal.js "^10.4.2" domexception "^4.0.0" escodegen "^2.0.0" form-data "^4.0.0" @@ -4928,12 +4880,12 @@ jsdom@^20.0.0: saxes "^6.0.0" symbol-tree "^3.2.4" tough-cookie "^4.1.2" - w3c-xmlserializer "^3.0.0" + w3c-xmlserializer "^4.0.0" webidl-conversions "^7.0.0" whatwg-encoding "^2.0.0" whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" - ws "^8.9.0" + ws "^8.11.0" xml-name-validator "^4.0.0" jsesc@^2.5.1: @@ -4967,16 +4919,16 @@ json-stable-stringify-without-jsonify@^1.0.1: integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" json5@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" - integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonc-parser@^3.0.0: version "3.2.0" @@ -5129,6 +5081,13 @@ lru-cache@^4.1.5: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -5170,10 +5129,10 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -marked@^4.0.19: - version "4.2.2" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.2.tgz#1d2075ad6cdfe42e651ac221c32d949a26c0672a" - integrity sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ== +marked@^4.2.4: + version "4.2.5" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.5.tgz#979813dfc1252cc123a79b71b095759a32f42a5d" + integrity sha512-jPueVhumq7idETHkb203WDD4fMA3yV9emQ5vLwop58lu8bTclMghBWcYAavlDqIEMaisADinV1TooIFCfqOsYQ== matrix-events-sdk@0.0.1: version "0.0.1" @@ -5283,10 +5242,10 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatc dependencies: brace-expansion "^1.1.7" -minimatch@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" - integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== +minimatch@^5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.2.tgz#0939d7d6f0898acbd1508abe534d1929368a8fff" + integrity sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg== dependencies: brace-expansion "^2.0.1" @@ -5327,9 +5286,9 @@ module-deps@^6.2.3: xtend "^4.0.0" mold-source-map@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/mold-source-map/-/mold-source-map-0.4.0.tgz#cf67e0b31c47ab9badb5c9c25651862127bb8317" - integrity sha512-Y0uA/sDKVuPgLd7BmaJOai+fqzjrOlR6vZgx5cJIvturI/xOPQPgbf3X7ZbzJd6MvqQ6ucIfK8dSteFyc2Mw2w== + version "0.4.1" + resolved "https://registry.yarnpkg.com/mold-source-map/-/mold-source-map-0.4.1.tgz#92e206393f9a3a7af0eb0c330493062f19dfe511" + integrity sha512-oPowVpTm8p3jsK2AKI+NzoS6TBKv7gWY/Hj4ZNh5YWiB3S4eP54y8vSEEgVUdrqgTbjwEzIunNAVQJGRW0bakQ== dependencies: convert-source-map "^1.1.0" through "~2.2.7" @@ -5389,9 +5348,9 @@ node-int64@^0.4.0: integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" - integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + version "2.0.8" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" + integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A== normalize-package-data@^2.5.0: version "2.5.0" @@ -5602,9 +5561,9 @@ parse-json@^5.0.0, parse-json@^5.2.0: lines-and-columns "^1.1.6" parse5@^7.0.0, parse5@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.1.tgz#4649f940ccfb95d8754f37f73078ea20afe0c746" - integrity sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg== + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== dependencies: entities "^4.4.0" @@ -5708,10 +5667,10 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== -prettier@2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.0.tgz#c7df58393c9ba77d6fba3921ae01faf994fb9dc9" - integrity sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA== +prettier@2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" + integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== pretty-format@^28.1.3: version "28.1.3" @@ -5913,13 +5872,6 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -qs@^6.9.6: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - querystring-es3@~0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -5983,9 +5935,9 @@ react-docgen@^5.4.0: strip-indent "^3.0.0" react-frame-component@^5.2.1: - version "5.2.3" - resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-5.2.3.tgz#2d5d1e29b23d5b915c839b44980d03bb9cafc453" - integrity sha512-r+h0o3r/uqOLNT724z4CRVkxQouKJvoi3OPfjqWACD30Y87rtEmeJrNZf1WYPGknn1Y8200HAjx7hY/dPUGgmA== + version "5.2.5" + resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-5.2.5.tgz#c16b12d9d0069f31a959a43ebef125440dfc201f" + integrity sha512-7PKQb/7r7o0fdKyHi47g09PfXcJf21BO+i6Z4h59KDaDu1nv6HIG+yLuPiLid0N6CAvmbHhHP8pdP+3WZuIIgg== react-is@^16.13.1: version "16.13.1" @@ -6093,10 +6045,10 @@ regenerator-runtime@^0.13.11: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -regenerator-transform@^0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" - integrity sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg== +regenerator-transform@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" + integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== dependencies: "@babel/runtime" "^7.8.4" @@ -6119,17 +6071,17 @@ regexpp@^3.2.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -regexpu-core@^5.1.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.2.1.tgz#a69c26f324c1e962e9ffd0b88b055caba8089139" - integrity sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ== +regexpu-core@^5.2.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.2.2.tgz#3e4e5d12103b64748711c3aad69934d7718e75fc" + integrity sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw== dependencies: regenerate "^1.4.2" regenerate-unicode-properties "^10.1.0" regjsgen "^0.7.1" regjsparser "^0.9.1" unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" regjsgen@^0.7.1: version "0.7.1" @@ -6498,7 +6450,7 @@ 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.prototype.trimend@^1.0.5: +string.prototype.trimend@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== @@ -6507,7 +6459,7 @@ string.prototype.trimend@^1.0.5: define-properties "^1.1.4" es-abstract "^1.20.4" -string.prototype.trimstart@^1.0.5: +string.prototype.trimstart@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== @@ -6635,9 +6587,9 @@ tapable@^2.2.0: integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== terser@^5.5.1: - version "5.16.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.0.tgz#29362c6f5506e71545c73b069ccd199bb28f7f54" - integrity sha512-KjTV81QKStSfwbNiwlBXfcgMcOloyuRdb62/iLFPGBcVNF4EXjhdYBhYHmbJpiBrVxZhDvltE11j+LBQUxEEJg== + version "5.16.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.1.tgz#5af3bc3d0f24241c7fb2024199d5c461a1075880" + integrity sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw== dependencies: "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" @@ -6879,13 +6831,13 @@ typedoc-plugin-missing-exports@^1.0.0: integrity sha512-7s6znXnuAj1eD9KYPyzVzR1lBF5nwAY8IKccP5sdoO9crG4lpd16RoFpLsh2PccJM+I2NASpr0+/NMka6ThwVA== typedoc@^0.23.20: - version "0.23.21" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.23.21.tgz#2a6b0e155f91ffa9689086706ad7e3e4bc11d241" - integrity sha512-VNE9Jv7BgclvyH9moi2mluneSviD43dCE9pY8RWkO88/DrEgJZk9KpUk7WO468c9WWs/+aG6dOnoH7ccjnErhg== + version "0.23.23" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.23.23.tgz#9cf95b03d2d40031d8978b55e88b0b968d69f512" + integrity sha512-cg1YQWj+/BU6wq74iott513U16fbrPCbyYs04PHZgvoKJIc6EY4xNobyDZh4KMfRGW8Yjv6wwIzQyoqopKOUGw== dependencies: lunr "^2.3.9" - marked "^4.0.19" - minimatch "^5.1.0" + marked "^4.2.4" + minimatch "^5.1.1" shiki "^0.11.1" typescript@^3.2.2: @@ -6894,9 +6846,9 @@ typescript@^3.2.2: integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== typescript@^4.5.3, typescript@^4.5.4: - version "4.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" - integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== + version "4.9.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" + integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== typeson-registry@^1.0.0-alpha.20: version "1.0.0-alpha.39" @@ -6976,10 +6928,10 @@ unicode-match-property-ecmascript@^2.0.0: unicode-canonical-property-names-ecmascript "^2.0.0" unicode-property-aliases-ecmascript "^2.0.0" -unicode-match-property-value-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" - integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== unicode-property-aliases-ecmascript@^2.0.0: version "2.1.0" @@ -7050,16 +7002,16 @@ util@~0.12.0: is-typed-array "^1.1.3" which-typed-array "^1.1.2" -uuid@7: - version "7.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" - integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== - uuid@8.3.2, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@9: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-to-istanbul@^9.0.0, v8-to-istanbul@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" @@ -7088,9 +7040,9 @@ void-elements@^2.0.1: integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== vscode-oniguruma@^1.6.1: - version "1.6.2" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz#aeb9771a2f1dbfc9083c8a7fdd9cccaa3f386607" - integrity sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA== + version "1.7.0" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" + integrity sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA== vscode-textmate@^6.0.0: version "6.0.0" @@ -7114,9 +7066,9 @@ vue-docgen-api@^3.26.0: vue-template-compiler "^2.0.0" vue-template-compiler@^2.0.0: - version "2.7.13" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.13.tgz#1520a5aa6d1af51dd0622824e79814f6e8cb7058" - integrity sha512-jYM6TClwDS9YqP48gYrtAtaOhRKkbYmbzE+Q51gX5YDr777n7tNI/IZk4QV4l/PjQPNh/FVa/E92sh/RqKMrog== + version "2.7.14" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz#4545b7dfb88090744c1577ae5ac3f964e61634b1" + integrity sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ== dependencies: de-indent "^1.0.2" he "^1.2.0" @@ -7128,10 +7080,10 @@ vue2-ace-editor@^0.0.15: dependencies: brace "^0.11.0" -w3c-xmlserializer@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923" - integrity sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg== +w3c-xmlserializer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" + integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== dependencies: xml-name-validator "^4.0.0" @@ -7274,7 +7226,7 @@ write-file-atomic@^4.0.1: imurmurhash "^0.1.4" signal-exit "^3.0.7" -ws@^8.9.0: +ws@^8.11.0: version "8.11.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== @@ -7309,6 +7261,11 @@ yallist@^2.1.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"