From 3a1897629ae56b13f220aaa8d16cb47206a28f5e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 28 Nov 2022 22:50:37 -0600 Subject: [PATCH 01/88] Prefer stable endpoint first --- src/client.ts | 52 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/src/client.ts b/src/client.ts index b091a31ec45..2e0492eb12d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9297,11 +9297,11 @@ export class MatrixClient extends TypedEventEmittererr).httpStatus === 400 && (err).errcode === "M_UNRECOGNIZED") { + return await this.http.authedRequest( + Method.Get, + path, + queryParams, + undefined, + { + prefix: "/_matrix/client/unstable/org.matrix.msc3030", + }, + ); + } + + throw err; + } } } From d3f08fec03d6805bf776a676451618a453cd8f2c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 30 Nov 2022 18:45:05 -0600 Subject: [PATCH 02/88] Add tests --- spec/unit/matrix-client.spec.ts | 175 ++++++++++++++++++++++++++++++-- src/client.ts | 11 +- 2 files changed, 174 insertions(+), 12 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index ba4be9f9b20..55f99d9af20 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -19,6 +19,12 @@ import { mocked } from "jest-mock"; import { logger } from "../../src/logger"; import { MatrixClient, ClientEvent } from "../../src/client"; import { Filter } from "../../src/filter"; +import { + Method, + ClientPrefix, + IRequestOpts, +} from "../../src/http-api"; +import { QueryDict } from "../../src/utils"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace"; import { EventType, @@ -52,6 +58,27 @@ jest.mock("../../src/webrtc/call", () => ({ supportsMatrixCall: jest.fn(() => false), })); +enum AnsiColorCode { + Red = 31, + Green = 32, + Yellow = 33, +} +// Color text in the terminal +function decorateStringWithAnsiColor(inputString: string, decorationColor: AnsiColorCode) { + return `\x1b[${decorationColor}m${inputString}\x1b[0m`; +} + +function convertQueryDictToStringRecord(queryDict?: QueryDict): Record { + if (!queryDict) { + return {}; + } + + return Object.entries(queryDict).reduce((resultant, [key, value]) => { + resultant[key] = String(value); + return resultant; + }, {}); +} + describe("MatrixClient", function() { const userId = "@alice:bar"; const identityServerUrl = "https://identity.server"; @@ -92,10 +119,11 @@ describe("MatrixClient", function() { let httpLookups: { method: string; path: string; + prefix?: string; data?: object; error?: object; expectBody?: object; - expectQueryParams?: object; + expectQueryParams?: QueryDict; thenCall?: Function; }[] = []; let acceptKeepalives: boolean; @@ -104,7 +132,14 @@ describe("MatrixClient", function() { method: string; path: string; } | null = null; - function httpReq(method, path, qp, data, prefix) { + function httpReq( + method: Method, + path: string, + queryParams?: QueryDict, + body?: Body, + requestOpts: IRequestOpts = {} + ) { + const { prefix } = requestOpts; if (path === KEEP_ALIVE_PATH && acceptKeepalives) { return Promise.resolve({ unstable_features: { @@ -135,17 +170,20 @@ 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]); }); } @@ -165,12 +203,15 @@ 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(() => {}); + // If you're seeing this then you forgot to handle at least 1 pending request. + const receivedRequest = decorateStringWithAnsiColor(`${method} ${prefix}${path}${new URLSearchParams(convertQueryDictToStringRecord(queryParams)).toString()}`, AnsiColorCode.Red); + const expectedRequest = decorateStringWithAnsiColor(`${next.method} ${next.prefix ?? ''}${next.path}${new URLSearchParams(convertQueryDictToStringRecord(next.expectQueryParams)).toString()}`, AnsiColorCode.Green); + throw new Error( + `A pending request was not handled: ${receivedRequest} ` + + `(next request expected was ${expectedRequest})\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() { @@ -231,6 +272,118 @@ 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"; + + it('should call stable endpoint', async () => { + httpLookups = [{ + method: "GET", + path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, + data: { event_id: eventId }, + expectQueryParams: { + ts: '0', + dir: 'f' + }, + }]; + + await client.timestampToEvent(roomId, 0, 'f'); + + expect(client.http.authedRequest.mock.calls.length).toStrictEqual(1); + const [method, path, queryParams,, { prefix }] = client.http.authedRequest.mock.calls[0]; + expect(method).toStrictEqual('GET'); + expect(prefix).toStrictEqual(ClientPrefix.V1); + expect(path).toStrictEqual( + `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event` + ); + expect(queryParams).toStrictEqual({ + ts: '0', + dir: 'f' + }); + }); + + it('should fallback to unstable endpoint when no support for stable endpoint', async () => { + httpLookups = [{ + method: "GET", + path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, + prefix: ClientPrefix.V1, + error: { + httpStatus: 404, + errcode: "M_UNRECOGNIZED" + }, + expectQueryParams: { + ts: '0', + dir: 'f' + }, + }, { + method: "GET", + path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, + prefix: unstableMsc3030Prefix, + data: { event_id: eventId }, + expectQueryParams: { + ts: '0', + dir: 'f' + }, + }]; + + await client.timestampToEvent(roomId, 0, 'f'); + + expect(client.http.authedRequest.mock.calls.length).toStrictEqual(2); + const [stableMethod, stablePath, stableQueryParams,, { prefix: stablePrefix }] = client.http.authedRequest.mock.calls[0]; + expect(stableMethod).toStrictEqual('GET'); + expect(stablePrefix).toStrictEqual(ClientPrefix.V1); + expect(stablePath).toStrictEqual( + `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event` + ); + expect(stableQueryParams).toStrictEqual({ + ts: '0', + dir: 'f' + }); + + const [unstableMethod, unstablePath, unstableQueryParams,, { prefix: unstablePrefix }] = client.http.authedRequest.mock.calls[1]; + expect(unstableMethod).toStrictEqual('GET'); + expect(unstablePrefix).toStrictEqual(unstableMsc3030Prefix); + expect(unstablePath).toStrictEqual( + `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event` + ); + expect(unstableQueryParams).toStrictEqual({ + ts: '0', + dir: 'f' + }); + }); + + it('should not fallback to unstable endpoint when stable endpoint returns an error', async () => { + httpLookups = [{ + method: "GET", + path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, + prefix: ClientPrefix.V1, + error: { + httpStatus: 500, + errcode: "Fake response error" + }, + expectQueryParams: { + ts: '0', + dir: 'f' + }, + }]; + + await expect(client.timestampToEvent(roomId, 0, 'f')).rejects.toBeDefined(); + + expect(client.http.authedRequest.mock.calls.length).toStrictEqual(1); + const [method, path, queryParams,, { prefix }] = client.http.authedRequest.mock.calls[0]; + expect(method).toStrictEqual('GET'); + expect(prefix).toStrictEqual(ClientPrefix.V1); + expect(path).toStrictEqual( + `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event` + ); + expect(queryParams).toStrictEqual({ + ts: '0', + dir: 'f' + }); + }); + }); + describe("sendEvent", () => { const roomId = "!room:example.org"; const body = "This is the body"; diff --git a/src/client.ts b/src/client.ts index 2e0492eb12d..5b19ef08d0e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9328,7 +9328,16 @@ export class MatrixClient extends TypedEventEmittererr).httpStatus === 400 && (err).errcode === "M_UNRECOGNIZED") { + 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. + (err).httpStatus === 404 + ) + ) { return await this.http.authedRequest( Method.Get, path, From d1ede036e21f696ab0a40c4f121f1e500ebd27a0 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 30 Nov 2022 18:51:49 -0600 Subject: [PATCH 03/88] Add return type --- spec/unit/matrix-client.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 55f99d9af20..e208e87efb7 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -64,7 +64,7 @@ enum AnsiColorCode { Yellow = 33, } // Color text in the terminal -function decorateStringWithAnsiColor(inputString: string, decorationColor: AnsiColorCode) { +function decorateStringWithAnsiColor(inputString: string, decorationColor: AnsiColorCode): string { return `\x1b[${decorationColor}m${inputString}\x1b[0m`; } From 9a731cdf4f421dc81977cc2552d2b597f51f5a61 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 30 Nov 2022 18:53:23 -0600 Subject: [PATCH 04/88] Add some comments --- spec/unit/matrix-client.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index e208e87efb7..237ffe4ac2d 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -63,11 +63,13 @@ enum AnsiColorCode { Green = 32, Yellow = 33, } -// Color text in the terminal +// Add color to text in the terminal output function decorateStringWithAnsiColor(inputString: string, decorationColor: AnsiColorCode): string { return `\x1b[${decorationColor}m${inputString}\x1b[0m`; } +// Utility function to ease the transition from our QueryDict type to a string record +// which we can use to stringify with URLSearchParams function convertQueryDictToStringRecord(queryDict?: QueryDict): Record { if (!queryDict) { return {}; From ad8bb5d2cd34bea4320a32b482dacd365e13d5f0 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 30 Nov 2022 19:01:20 -0600 Subject: [PATCH 05/88] Fix lints --- spec/unit/matrix-client.spec.ts | 63 +++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 237ffe4ac2d..b8ee2eac938 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -139,7 +139,7 @@ describe("MatrixClient", function() { path: string, queryParams?: QueryDict, body?: Body, - requestOpts: IRequestOpts = {} + requestOpts: IRequestOpts = {}, ) { const { prefix } = requestOpts; if (path === KEEP_ALIVE_PATH && acceptKeepalives) { @@ -205,9 +205,22 @@ describe("MatrixClient", function() { } return Promise.resolve(next.data); } + + const receivedRequestQueryString = new URLSearchParams( + convertQueryDictToStringRecord(queryParams), + ).toString(); + const receivedRequest = decorateStringWithAnsiColor( + `${method} ${prefix}${path}${receivedRequestQueryString}`, + AnsiColorCode.Red, + ); + const expectedQueryString = new URLSearchParams( + convertQueryDictToStringRecord(next.expectQueryParams), + ).toString(); + const expectedRequest = decorateStringWithAnsiColor( + `${next.method} ${next.prefix ?? ''}${next.path}${expectedQueryString}`, + AnsiColorCode.Green, + ); // If you're seeing this then you forgot to handle at least 1 pending request. - const receivedRequest = decorateStringWithAnsiColor(`${method} ${prefix}${path}${new URLSearchParams(convertQueryDictToStringRecord(queryParams)).toString()}`, AnsiColorCode.Red); - const expectedRequest = decorateStringWithAnsiColor(`${next.method} ${next.prefix ?? ''}${next.path}${new URLSearchParams(convertQueryDictToStringRecord(next.expectQueryParams)).toString()}`, AnsiColorCode.Green); throw new Error( `A pending request was not handled: ${receivedRequest} ` + `(next request expected was ${expectedRequest})\n` + @@ -286,7 +299,7 @@ describe("MatrixClient", function() { data: { event_id: eventId }, expectQueryParams: { ts: '0', - dir: 'f' + dir: 'f', }, }]; @@ -297,11 +310,11 @@ describe("MatrixClient", function() { expect(method).toStrictEqual('GET'); expect(prefix).toStrictEqual(ClientPrefix.V1); expect(path).toStrictEqual( - `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event` + `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, ); expect(queryParams).toStrictEqual({ ts: '0', - dir: 'f' + dir: 'f', }); }); @@ -312,11 +325,11 @@ describe("MatrixClient", function() { prefix: ClientPrefix.V1, error: { httpStatus: 404, - errcode: "M_UNRECOGNIZED" + errcode: "M_UNRECOGNIZED", }, expectQueryParams: { ts: '0', - dir: 'f' + dir: 'f', }, }, { method: "GET", @@ -325,33 +338,45 @@ describe("MatrixClient", function() { data: { event_id: eventId }, expectQueryParams: { ts: '0', - dir: 'f' + dir: 'f', }, }]; await client.timestampToEvent(roomId, 0, 'f'); expect(client.http.authedRequest.mock.calls.length).toStrictEqual(2); - const [stableMethod, stablePath, stableQueryParams,, { prefix: stablePrefix }] = client.http.authedRequest.mock.calls[0]; + const [ + stableMethod, + stablePath, + stableQueryParams, + , + { prefix: stablePrefix }, + ] = client.http.authedRequest.mock.calls[0]; expect(stableMethod).toStrictEqual('GET'); expect(stablePrefix).toStrictEqual(ClientPrefix.V1); expect(stablePath).toStrictEqual( - `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event` + `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, ); expect(stableQueryParams).toStrictEqual({ ts: '0', - dir: 'f' + dir: 'f', }); - const [unstableMethod, unstablePath, unstableQueryParams,, { prefix: unstablePrefix }] = client.http.authedRequest.mock.calls[1]; + const [ + unstableMethod, + unstablePath, + unstableQueryParams, + , + { prefix: unstablePrefix }, + ] = client.http.authedRequest.mock.calls[1]; expect(unstableMethod).toStrictEqual('GET'); expect(unstablePrefix).toStrictEqual(unstableMsc3030Prefix); expect(unstablePath).toStrictEqual( - `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event` + `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, ); expect(unstableQueryParams).toStrictEqual({ ts: '0', - dir: 'f' + dir: 'f', }); }); @@ -362,11 +387,11 @@ describe("MatrixClient", function() { prefix: ClientPrefix.V1, error: { httpStatus: 500, - errcode: "Fake response error" + errcode: "Fake response error", }, expectQueryParams: { ts: '0', - dir: 'f' + dir: 'f', }, }]; @@ -377,11 +402,11 @@ describe("MatrixClient", function() { expect(method).toStrictEqual('GET'); expect(prefix).toStrictEqual(ClientPrefix.V1); expect(path).toStrictEqual( - `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event` + `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, ); expect(queryParams).toStrictEqual({ ts: '0', - dir: 'f' + dir: 'f', }); }); }); From 9a98e8008f3699164609819596daad49df86be12 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 30 Nov 2022 19:13:29 -0600 Subject: [PATCH 06/88] Fix relevant strict ts error --- spec/unit/matrix-client.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index b8ee2eac938..180f44bd229 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -78,7 +78,7 @@ function convertQueryDictToStringRecord(queryDict?: QueryDict): Record { resultant[key] = String(value); return resultant; - }, {}); + }, {} as Record); } describe("MatrixClient", function() { From bf78a64d821ab6581932624a27dbe00185ebac2b Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 8 Dec 2022 17:47:56 -0600 Subject: [PATCH 07/88] Remove console coloring in favor of future PR See https://github.com/matrix-org/matrix-js-sdk/pull/2915#discussion_r1041539703 --- spec/unit/matrix-client.spec.ts | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 180f44bd229..f1cb7a67b07 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -58,16 +58,6 @@ jest.mock("../../src/webrtc/call", () => ({ supportsMatrixCall: jest.fn(() => false), })); -enum AnsiColorCode { - Red = 31, - Green = 32, - Yellow = 33, -} -// Add color to text in the terminal output -function decorateStringWithAnsiColor(inputString: string, decorationColor: AnsiColorCode): string { - return `\x1b[${decorationColor}m${inputString}\x1b[0m`; -} - // Utility function to ease the transition from our QueryDict type to a string record // which we can use to stringify with URLSearchParams function convertQueryDictToStringRecord(queryDict?: QueryDict): Record { @@ -209,21 +199,15 @@ describe("MatrixClient", function() { const receivedRequestQueryString = new URLSearchParams( convertQueryDictToStringRecord(queryParams), ).toString(); - const receivedRequest = decorateStringWithAnsiColor( - `${method} ${prefix}${path}${receivedRequestQueryString}`, - AnsiColorCode.Red, - ); + const receivedRequestDebugString = `${method} ${prefix}${path}${receivedRequestQueryString}`; const expectedQueryString = new URLSearchParams( convertQueryDictToStringRecord(next.expectQueryParams), ).toString(); - const expectedRequest = decorateStringWithAnsiColor( - `${next.method} ${next.prefix ?? ''}${next.path}${expectedQueryString}`, - AnsiColorCode.Green, - ); + 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: ${receivedRequest} ` + - `(next request expected was ${expectedRequest})\n` + + `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.`, ); From c953fc9fb7f8b02848d552b3045d3985322ec2f8 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 8 Dec 2022 17:56:53 -0600 Subject: [PATCH 08/88] Update casing See https://github.com/matrix-org/matrix-js-sdk/pull/2915#discussion_r1041542066 --- spec/unit/matrix-client.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index f1cb7a67b07..56124449c35 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -274,7 +274,7 @@ describe("MatrixClient", function() { describe('timestampToEvent', () => { const roomId = '!room:server.org'; const eventId = "$eventId:example.org"; - const unstableMsc3030Prefix = "/_matrix/client/unstable/org.matrix.msc3030"; + const unstableMSC3030Prefix = "/_matrix/client/unstable/org.matrix.msc3030"; it('should call stable endpoint', async () => { httpLookups = [{ @@ -318,7 +318,7 @@ describe("MatrixClient", function() { }, { method: "GET", path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, - prefix: unstableMsc3030Prefix, + prefix: unstableMSC3030Prefix, data: { event_id: eventId }, expectQueryParams: { ts: '0', @@ -354,7 +354,7 @@ describe("MatrixClient", function() { { prefix: unstablePrefix }, ] = client.http.authedRequest.mock.calls[1]; expect(unstableMethod).toStrictEqual('GET'); - expect(unstablePrefix).toStrictEqual(unstableMsc3030Prefix); + expect(unstablePrefix).toStrictEqual(unstableMSC3030Prefix); expect(unstablePath).toStrictEqual( `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, ); From 9841f92415b925da7d9bac5da43681c98377431c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 8 Dec 2022 18:37:33 -0600 Subject: [PATCH 09/88] Fix some eslint --- src/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index b37362d9168..daac552fc62 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9311,9 +9311,9 @@ export class MatrixClient extends TypedEventEmitter Date: Wed, 30 Nov 2022 10:24:28 +0000 Subject: [PATCH 10/88] Resume to-device message queue after resumed sync --- src/ToDeviceMessageQueue.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/ToDeviceMessageQueue.ts b/src/ToDeviceMessageQueue.ts index e78c46ba200..986eb5a9f90 100644 --- a/src/ToDeviceMessageQueue.ts +++ b/src/ToDeviceMessageQueue.ts @@ -16,9 +16,10 @@ limitations under the License. import { ToDeviceMessageId } from "./@types/event"; import { logger } from "./logger"; -import { MatrixError, MatrixClient } from "./matrix"; +import { MatrixError, MatrixClient, ClientEvent } from "./matrix"; import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage"; import { MatrixScheduler } from "./scheduler"; +import { SyncState } from "./sync"; const MAX_BATCH_SIZE = 20; @@ -37,12 +38,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 +135,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(); + } + }; } From 11ac3d9e58b6f2f3e7f390f0fbd315fcf2fb589d Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 12 Dec 2022 16:13:21 +0000 Subject: [PATCH 11/88] Add unit tests --- spec/unit/ToDeviceMessageQueue.spec.ts | 106 +++++++++++++++++++++++++ src/ToDeviceMessageQueue.ts | 3 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 spec/unit/ToDeviceMessageQueue.spec.ts 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/src/ToDeviceMessageQueue.ts b/src/ToDeviceMessageQueue.ts index 986eb5a9f90..ec5922bb68c 100644 --- a/src/ToDeviceMessageQueue.ts +++ b/src/ToDeviceMessageQueue.ts @@ -16,7 +16,8 @@ limitations under the License. import { ToDeviceMessageId } from "./@types/event"; import { logger } from "./logger"; -import { MatrixError, MatrixClient, ClientEvent } 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"; From 70a033c2fdad82e5e041558ae4dd9d774503e257 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 13 Dec 2022 16:26:14 -0600 Subject: [PATCH 12/88] Prettier fixes --- spec/unit/matrix-client.spec.ts | 173 +++++++++++++++----------------- src/client.ts | 65 +++++------- 2 files changed, 107 insertions(+), 131 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 03d9d0d2445..0be036b0fc2 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -220,20 +220,18 @@ describe("MatrixClient", function () { return Promise.resolve(next.data); } - const receivedRequestQueryString = new URLSearchParams( - convertQueryDictToStringRecord(queryParams), - ).toString(); + const receivedRequestQueryString = new URLSearchParams(convertQueryDictToStringRecord(queryParams)).toString(); const receivedRequestDebugString = `${method} ${prefix}${path}${receivedRequestQueryString}`; const expectedQueryString = new URLSearchParams( convertQueryDictToStringRecord(next.expectQueryParams), ).toString(); - const expectedRequestDebugString = `${next.method} ${next.prefix ?? ''}${next.path}${expectedQueryString}`; + 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.`, + `(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.`, ); } @@ -317,126 +315,117 @@ describe("MatrixClient", function () { client.stopClient(); }); - describe('timestampToEvent', () => { - const roomId = '!room:server.org'; + describe("timestampToEvent", () => { + const roomId = "!room:server.org"; const eventId = "$eventId:example.org"; const unstableMSC3030Prefix = "/_matrix/client/unstable/org.matrix.msc3030"; - it('should call stable endpoint', async () => { - httpLookups = [{ - method: "GET", - path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, - data: { event_id: eventId }, - expectQueryParams: { - ts: '0', - dir: 'f', + it("should call stable endpoint", async () => { + httpLookups = [ + { + method: "GET", + path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, + data: { event_id: eventId }, + expectQueryParams: { + ts: "0", + dir: "f", + }, }, - }]; + ]; await client.timestampToEvent(roomId, 0, Direction.Forward); expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(1); - const [method, path, queryParams,, { prefix }] = mocked(client.http.authedRequest).mock.calls[0]; - expect(method).toStrictEqual('GET'); + const [method, path, queryParams, , { prefix }] = mocked(client.http.authedRequest).mock.calls[0]; + expect(method).toStrictEqual("GET"); expect(prefix).toStrictEqual(ClientPrefix.V1); - expect(path).toStrictEqual( - `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, - ); + expect(path).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); expect(queryParams).toStrictEqual({ - ts: '0', - dir: 'f', + ts: "0", + dir: "f", }); }); - it('should fallback to unstable endpoint when no support for stable endpoint', async () => { - httpLookups = [{ - method: "GET", - path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, - prefix: ClientPrefix.V1, - error: { - httpStatus: 404, - errcode: "M_UNRECOGNIZED", - }, - expectQueryParams: { - ts: '0', - dir: 'f', + it("should fallback to unstable endpoint when no support for stable endpoint", async () => { + httpLookups = [ + { + method: "GET", + path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, + prefix: ClientPrefix.V1, + error: { + httpStatus: 404, + errcode: "M_UNRECOGNIZED", + }, + expectQueryParams: { + ts: "0", + dir: "f", + }, }, - }, { - method: "GET", - path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, - prefix: unstableMSC3030Prefix, - data: { event_id: eventId }, - expectQueryParams: { - ts: '0', - dir: 'f', + { + method: "GET", + path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, + prefix: unstableMSC3030Prefix, + data: { event_id: eventId }, + expectQueryParams: { + ts: "0", + dir: "f", + }, }, - }]; + ]; await client.timestampToEvent(roomId, 0, Direction.Forward); expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(2); - const [ - stableMethod, - stablePath, - stableQueryParams, - , - { prefix: stablePrefix }, - ] = mocked(client.http.authedRequest).mock.calls[0]; - expect(stableMethod).toStrictEqual('GET'); + const [stableMethod, stablePath, stableQueryParams, , { prefix: stablePrefix }] = mocked( + client.http.authedRequest, + ).mock.calls[0]; + expect(stableMethod).toStrictEqual("GET"); expect(stablePrefix).toStrictEqual(ClientPrefix.V1); - expect(stablePath).toStrictEqual( - `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, - ); + expect(stablePath).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); expect(stableQueryParams).toStrictEqual({ - ts: '0', - dir: 'f', + ts: "0", + dir: "f", }); - const [ - unstableMethod, - unstablePath, - unstableQueryParams, - , - { prefix: unstablePrefix }, - ] = mocked(client.http.authedRequest).mock.calls[1]; - expect(unstableMethod).toStrictEqual('GET'); + const [unstableMethod, unstablePath, unstableQueryParams, , { prefix: unstablePrefix }] = mocked( + client.http.authedRequest, + ).mock.calls[1]; + expect(unstableMethod).toStrictEqual("GET"); expect(unstablePrefix).toStrictEqual(unstableMSC3030Prefix); - expect(unstablePath).toStrictEqual( - `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, - ); + expect(unstablePath).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); expect(unstableQueryParams).toStrictEqual({ - ts: '0', - dir: 'f', + ts: "0", + dir: "f", }); }); - it('should not fallback to unstable endpoint when stable endpoint returns an error', async () => { - httpLookups = [{ - method: "GET", - path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, - prefix: ClientPrefix.V1, - error: { - httpStatus: 500, - errcode: "Fake response error", - }, - expectQueryParams: { - ts: '0', - dir: 'f', + it("should not fallback to unstable endpoint when stable endpoint returns an error", async () => { + httpLookups = [ + { + method: "GET", + path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, + prefix: ClientPrefix.V1, + error: { + httpStatus: 500, + errcode: "Fake response error", + }, + expectQueryParams: { + ts: "0", + dir: "f", + }, }, - }]; + ]; await expect(client.timestampToEvent(roomId, 0, Direction.Forward)).rejects.toBeDefined(); expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(1); - const [method, path, queryParams,, { prefix }] = mocked(client.http.authedRequest).mock.calls[0]; - expect(method).toStrictEqual('GET'); + const [method, path, queryParams, , { prefix }] = mocked(client.http.authedRequest).mock.calls[0]; + expect(method).toStrictEqual("GET"); expect(prefix).toStrictEqual(ClientPrefix.V1); - expect(path).toStrictEqual( - `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, - ); + expect(path).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); expect(queryParams).toStrictEqual({ - ts: '0', - dir: 'f', + ts: "0", + dir: "f", }); }); }); diff --git a/src/client.ts b/src/client.ts index be5c7c5fb1f..10f59eca50e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -480,9 +480,9 @@ export interface ICapability { enabled: boolean; } -export interface IChangePasswordCapability extends ICapability { } +export interface IChangePasswordCapability extends ICapability {} -export interface IThreadsCapability extends ICapability { } +export interface IThreadsCapability extends ICapability {} interface ICapabilities { [key: string]: any; @@ -1244,12 +1244,12 @@ export class MatrixClient extends TypedEventEmittererr).errcode === "M_UNRECOGNIZED" && ( - // XXX: The 400 status code check should be removed in the future - // when Synapse is compliant with MSC3743. - (err).httpStatus === 400 || + (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. - (err).httpStatus === 404 - ) + (err).httpStatus === 404) ) { - return await this.http.authedRequest( - Method.Get, - path, - queryParams, - undefined, - { - prefix: "/_matrix/client/unstable/org.matrix.msc3030", - }, - ); + return await this.http.authedRequest(Method.Get, path, queryParams, undefined, { + prefix: "/_matrix/client/unstable/org.matrix.msc3030", + }); } throw err; @@ -9338,12 +9325,12 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri hasReadEvent = thread ? thread.hasUserReadEvent(cli.getUserId()!, event.getId()!) : // If the thread object does not exist in the room yet, we don't - // want to calculate notification for this event yet. We have not - // restored the read receipts yet and can't accurately calculate - // highlight notifications at this stage. - // - // This issue can likely go away when MSC3874 is implemented - true; + // want to calculate notification for this event yet. We have not + // restored the read receipts yet and can't accurately calculate + // highlight notifications at this stage. + // + // This issue can likely go away when MSC3874 is implemented + true; } else { hasReadEvent = room.hasUserReadEvent(cli.getUserId()!, event.getId()!); } From a0aa5074ed95514737f7a3e58c912a1be849ea3f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 13 Dec 2022 16:37:47 -0600 Subject: [PATCH 13/88] Fix prefix lint See https://github.com/matrix-org/matrix-js-sdk/pull/2915#discussion_r1043951639 --- spec/unit/matrix-client.spec.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 0be036b0fc2..f5c44006726 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -336,7 +336,8 @@ describe("MatrixClient", function () { await client.timestampToEvent(roomId, 0, Direction.Forward); expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(1); - const [method, path, queryParams, , { prefix }] = mocked(client.http.authedRequest).mock.calls[0]; + const [method, path, queryParams, , { prefix } = { prefix: undefined }] = mocked(client.http.authedRequest) + .mock.calls[0]; expect(method).toStrictEqual("GET"); expect(prefix).toStrictEqual(ClientPrefix.V1); expect(path).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); @@ -376,9 +377,8 @@ describe("MatrixClient", function () { await client.timestampToEvent(roomId, 0, Direction.Forward); expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(2); - const [stableMethod, stablePath, stableQueryParams, , { prefix: stablePrefix }] = mocked( - client.http.authedRequest, - ).mock.calls[0]; + const [stableMethod, stablePath, stableQueryParams, , { prefix: stablePrefix } = { prefix: undefined }] = + mocked(client.http.authedRequest).mock.calls[0]; expect(stableMethod).toStrictEqual("GET"); expect(stablePrefix).toStrictEqual(ClientPrefix.V1); expect(stablePath).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); @@ -387,9 +387,13 @@ describe("MatrixClient", function () { dir: "f", }); - const [unstableMethod, unstablePath, unstableQueryParams, , { prefix: unstablePrefix }] = mocked( - client.http.authedRequest, - ).mock.calls[1]; + const [ + unstableMethod, + unstablePath, + unstableQueryParams, + , + { prefix: unstablePrefix } = { prefix: undefined }, + ] = mocked(client.http.authedRequest).mock.calls[1]; expect(unstableMethod).toStrictEqual("GET"); expect(unstablePrefix).toStrictEqual(unstableMSC3030Prefix); expect(unstablePath).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); @@ -419,7 +423,8 @@ describe("MatrixClient", function () { await expect(client.timestampToEvent(roomId, 0, Direction.Forward)).rejects.toBeDefined(); expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(1); - const [method, path, queryParams, , { prefix }] = mocked(client.http.authedRequest).mock.calls[0]; + const [method, path, queryParams, , { prefix } = { prefix: undefined }] = mocked(client.http.authedRequest) + .mock.calls[0]; expect(method).toStrictEqual("GET"); expect(prefix).toStrictEqual(ClientPrefix.V1); expect(path).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); From b2a10e6db3231f20502dfdfcfb435886bad06a9c Mon Sep 17 00:00:00 2001 From: Kerry Date: Wed, 14 Dec 2022 17:14:21 +1300 Subject: [PATCH 14/88] Support MSC3391: Account data deletion (#2967) * add deleteAccountData endpoint * check server support and test * test current state of memorystore * interpret account data events with empty content as deleted * add handling for (future) stable version of endpoint * add getSafeUserId * user getSafeUserId in deleteAccountData * better jsdoc for throws documentation --- spec/unit/matrix-client.spec.ts | 82 +++++++++++++++++++++++++++++++++ spec/unit/stores/memory.spec.ts | 65 ++++++++++++++++++++++++++ src/client.ts | 32 +++++++++++++ src/feature.ts | 4 ++ src/store/memory.ts | 8 +++- 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 spec/unit/stores/memory.spec.ts diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index b13dfc125be..68c6ded9fda 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -57,6 +57,7 @@ import { import { IOlmDevice } from "../../src/crypto/algorithms/megolm"; import { QueryDict } from "../../src/utils"; import { SyncState } from "../../src/sync"; +import * as featureUtils from "../../src/feature"; jest.useFakeTimers(); @@ -281,6 +282,23 @@ describe("MatrixClient", function () { client.stopClient(); }); + 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"; @@ -1828,4 +1846,68 @@ 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, {}); + }); + }); }); 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/src/client.ts b/src/client.ts index 83c4ac2cd12..bd21b854b89 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1672,6 +1672,20 @@ 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) diff --git a/src/feature.ts b/src/feature.ts index d555a860bd9..158e1f7ee1f 100644 --- a/src/feature.ts +++ b/src/feature.ts @@ -26,6 +26,7 @@ export enum Feature { Thread = "Thread", ThreadUnreadNotifications = "ThreadUnreadNotifications", LoginTokenRequest = "LoginTokenRequest", + AccountDataDeletion = "AccountDataDeletion", } type FeatureSupportCondition = { @@ -45,6 +46,9 @@ const featureSupportResolver: Record = { [Feature.LoginTokenRequest]: { unstablePrefixes: ["org.matrix.msc3882"], }, + [Feature.AccountDataDeletion]: { + unstablePrefixes: ["org.matrix.msc3391"], + }, }; export async function buildFeatureSupportMap(versions: IServerVersions): Promise> { diff --git a/src/store/memory.ts b/src/store/memory.ts index 782d7edef77..025a632aaf2 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -286,7 +286,13 @@ export class MemoryStore implements IStore { */ public storeAccountDataEvents(events: MatrixEvent[]): void { events.forEach((event) => { - 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; + } }); } From b765b18381064a147da2bb1ddd91585a64c77578 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 14 Dec 2022 11:46:18 +0100 Subject: [PATCH 15/88] Add prettier formatting to .git-blame-ignore-revs (#2979) --- .git-blame-ignore-revs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From df42014ef5a0581ec887305a6fe2b4004c1e95d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Dec 2022 10:55:40 +0000 Subject: [PATCH 16/88] Update dependency @types/jest to v29.2.4 (#2980) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index c4d494579ad..734ff3dc39d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1717,9 +1717,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.4" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.4.tgz#9c155c4b81c9570dbd183eb8604aa0ae80ba5a5b" + integrity sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -1743,7 +1743,12 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/node@*", "@types/node@18": +"@types/node@*": + version "18.11.15" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.15.tgz#de0e1fbd2b22b962d45971431e2ae696643d3f5d" + integrity sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw== + +"@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== @@ -1799,9 +1804,9 @@ 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.17" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.17.tgz#5672e5621f8e0fca13f433a8017aae4b7a2a03e7" + integrity sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g== dependencies: "@types/yargs-parser" "*" @@ -2636,12 +2641,7 @@ 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: +ci-info@^3.2.0, 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== From 15ef8fabb7d3deae7ff098bf4a9f4687d1602995 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 14 Dec 2022 11:02:02 +0000 Subject: [PATCH 17/88] Introduce a mechanism for using the rust-crypto-sdk (#2969) This PR introduces MatrixClient.initRustCrypto, which is similar to initCrypto, except that it will use the Rust crypto SDK instead of the old libolm-based implementation. This is very much not something you want to use in production code right now, because the integration with the rust sdk is extremely skeletal and almost everything crypto-related will raise an exception rather than doing anything useful. It is, however, enough to demonstrate the loading of the wasmified rust sdk in element web, and a react sdk with light modifications can successfully log in and out. Part of vector-im/element-web#21972. --- package.json | 1 + spec/integ/rust-crypto.spec.ts | 90 ++++++++++++++++++++++++++++++++++ src/client.ts | 76 ++++++++++++++++++++++++++++ src/rust-crypto/constants.ts | 18 +++++++ src/rust-crypto/index.ts | 41 ++++++++++++++++ src/rust-crypto/rust-crypto.ts | 60 +++++++++++++++++++++++ yarn.lock | 5 ++ 7 files changed, 291 insertions(+) create mode 100644 spec/integ/rust-crypto.spec.ts create mode 100644 src/rust-crypto/constants.ts create mode 100644 src/rust-crypto/index.ts create mode 100644 src/rust-crypto/rust-crypto.ts diff --git a/package.json b/package.json index 128de735ea3..95ef03ad0f6 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,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", 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/src/client.ts b/src/client.ts index bd21b854b89..832df1e1fc8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -211,6 +211,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; @@ -1657,6 +1658,41 @@ 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 => { + logger.error(`Failed to remove IndexedDB instance ${dbname}: ${e}`); + reject(new Error(`Error clearing storage: ${e}`)); + }; + req.onblocked = (e): void => { + logger.info(`cannot yet remove IndexedDB instance ${dbname}`); + //reject(new Error(`Error clearing storage: ${e}`)); + }; + }); + await prom; + } + }; + promises.push(deleteRustSdkStore()); + return Promise.all(promises).then(); // .then to fix types } @@ -2065,6 +2101,46 @@ 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(userId, deviceId); + } + /** * Is end-to-end crypto enabled for this client. * @returns True if end-to-end is enabled. 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..7485836cdc2 --- /dev/null +++ b/src/rust-crypto/index.ts @@ -0,0 +1,41 @@ +/* +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"; + +export async function initRustCrypto(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, 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..07b7ea3b9d9 --- /dev/null +++ b/src/rust-crypto/rust-crypto.ts @@ -0,0 +1,60 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; + +import { IEventDecryptionResult } from "../@types/crypto"; +import { MatrixEvent } from "../models/event"; +import { CryptoBackend } from "../common-crypto/CryptoBackend"; + +// import { logger } from "../logger"; + +/** + * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. + */ +export class RustCrypto implements CryptoBackend { + public globalBlacklistUnverifiedDevices = false; + public globalErrorOnUnknownDevices = false; + + /** whether stop() has been called */ + private stopped = false; + + public constructor(private readonly olmMachine: RustSdkCryptoJs.OlmMachine, _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; + } +} diff --git a/yarn.lock b/yarn.lock index 734ff3dc39d..4de7a48c96a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1432,6 +1432,11 @@ 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" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984" From 39e127b4e344691a19e44067fcee2bb9bf52b831 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 14 Dec 2022 13:01:52 +0100 Subject: [PATCH 18/88] Write test to validate #2971 (#2972) --- .../matrix-client-event-timeline.spec.ts | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index b02955511e9..5edd5c4b7d2 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -1018,6 +1018,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"; From f8bf6083dedc24298f6b5f97b736b1679996cc71 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 22:05:05 -0700 Subject: [PATCH 19/88] Update dependency uuid to v9 (#2988) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 95ef03ad0f6..e325aa0430c 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "qs": "^6.9.6", "sdp-transform": "^2.14.1", "unhomoglyph": "^1.0.6", - "uuid": "7" + "uuid": "9" }, "devDependencies": { "@babel/cli": "^7.12.10", diff --git a/yarn.lock b/yarn.lock index 4de7a48c96a..8224d656914 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7055,16 +7055,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" From 7b96c730b86e3c503e7a930ae2089baff003e77d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 22:05:22 -0700 Subject: [PATCH 20/88] Update typescript-eslint monorepo to v5.46.0 (#2985) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 98 +++++++++++++++++++++++++++---------------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8224d656914..fa22b81c61a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1816,13 +1816,13 @@ "@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.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz#098abb4c9354e19f460d57ab18bff1f676a6cff0" + integrity sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA== 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.46.1" + "@typescript-eslint/type-utils" "5.46.1" + "@typescript-eslint/utils" "5.46.1" debug "^4.3.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" @@ -1831,71 +1831,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.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.46.1.tgz#1fc8e7102c1141eb64276c3b89d70da8c0ba5699" + integrity sha512-RelQ5cGypPh4ySAtfIMBzBGyrNerQcmfA1oJvPj5f+H4jI59rl9xxpn4bonC0tQvUKOEN7eGBFWxFLK3Xepneg== 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.46.1" + "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/typescript-estree" "5.46.1" 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.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz#70af8425c79bbc1178b5a63fb51102ddf48e104a" + integrity sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA== dependencies: - "@typescript-eslint/types" "5.45.0" - "@typescript-eslint/visitor-keys" "5.45.0" + "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/visitor-keys" "5.46.1" -"@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.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz#195033e4b30b51b870dfcf2828e88d57b04a11cc" + integrity sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng== dependencies: - "@typescript-eslint/typescript-estree" "5.45.0" - "@typescript-eslint/utils" "5.45.0" + "@typescript-eslint/typescript-estree" "5.46.1" + "@typescript-eslint/utils" "5.46.1" 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.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.46.1.tgz#4e9db2107b9a88441c4d5ecacde3bb7a5ebbd47e" + integrity sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w== -"@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.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz#5358088f98a8f9939355e0996f9c8f41c25eced2" + integrity sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg== dependencies: - "@typescript-eslint/types" "5.45.0" - "@typescript-eslint/visitor-keys" "5.45.0" + "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/visitor-keys" "5.46.1" 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.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.46.1.tgz#7da3c934d9fd0eb4002a6bb3429f33298b469b4a" + integrity sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA== 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.46.1" + "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/typescript-estree" "5.46.1" 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.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz#126cc6fe3c0f83608b2b125c5d9daced61394242" + integrity sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg== dependencies: - "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/types" "5.46.1" eslint-visitor-keys "^3.3.0" JSONStream@^1.0.3: @@ -3679,9 +3679,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.14.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.14.0.tgz#107f69d7295b11e0fccc264e1fc6389f623731ce" + integrity sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg== dependencies: reusify "^1.0.4" From c973b26fa26ecc655ac7cb5f081358e88bed6b36 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 22:06:40 -0700 Subject: [PATCH 21/88] Update all non-major dependencies (#2981) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- yarn.lock | 62 ++++++++++++++++++++++++++-------------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index e325aa0430c..e3216884fc3 100644 --- a/package.json +++ b/package.json @@ -98,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", @@ -114,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/yarn.lock b/yarn.lock index fa22b81c61a..232e98e6ced 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1102,7 +1102,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== @@ -3404,12 +3404,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" @@ -3463,10 +3463,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" @@ -5176,9 +5176,9 @@ makeerror@1.0.12: 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== + version "4.2.4" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.4.tgz#5a4ce6c7a1ae0c952601fce46376ee4cf1797e1c" + integrity sha512-Wcc9ikX7Q5E4BYDPvh1C6QNSxrjC9tBgz+A/vAhp59KXUgachw++uMvMKiSW8oA85nopmPZcEvBoex/YLMsiyA== matrix-events-sdk@0.0.1: version "0.0.1" @@ -5289,9 +5289,9 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatc 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== + version "5.1.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.1.tgz#6c9dffcf9927ff2a31e74b5af11adf8b9604b022" + integrity sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g== dependencies: brace-expansion "^2.0.1" @@ -5713,10 +5713,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" @@ -6640,9 +6640,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" @@ -6884,9 +6884,9 @@ 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.22" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.23.22.tgz#e25281ca816cd92ecfdaf3ec336d27e7bebb69ac" + integrity sha512-5sJkjK60xp8A7YpcYniu3+Wf0QcgojEnhzHuCN+CkdpQkKRhOspon/9+sGTkGI8kjVkZs3KHrhltpQyVhRMVfw== dependencies: lunr "^2.3.9" marked "^4.0.19" @@ -6899,9 +6899,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" @@ -7093,9 +7093,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" From a04800f030998c5f9eb953c39fd62ae6970f1285 Mon Sep 17 00:00:00 2001 From: Till Faelligen <2353100+S7evinK@users.noreply.github.com> Date: Fri, 16 Dec 2022 11:49:31 +0100 Subject: [PATCH 22/88] Fix issue where no unsubs are sent when switching rooms --- src/sliding-sync.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index b219dad1159..7287489d30e 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -397,6 +397,10 @@ export class SlidingSync extends TypedEventEmitter Date: Fri, 16 Dec 2022 13:30:40 +0100 Subject: [PATCH 23/88] Add tests for custom subscriptions --- spec/integ/sliding-sync.spec.ts | 96 +++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) 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", () => { From 96ee5b1256b47971a0e05d978c8bd2908718cd21 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 16 Dec 2022 18:19:36 +0000 Subject: [PATCH 24/88] Close all streams when a call ends We didn't close streams in group calls (presumably from back when we used the same stream for all calls rather than cloning?) but this left stray screenshare streams in the mediahandler when a participant left whilst we were screensharing. Fixes https://github.com/vector-im/element-call/issues/742 --- src/webrtc/call.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index e9276f44fc8..d6fde54ed94 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -2475,18 +2475,20 @@ export class MatrixCall extends TypedEventEmitter Date: Mon, 19 Dec 2022 11:32:37 +0100 Subject: [PATCH 25/88] Threads are missing from the timeline (#2996) --- .../matrix-client-event-timeline.spec.ts | 55 +++++++++++++++++++ src/client.ts | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 5edd5c4b7d2..0c553e77ff5 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -1016,6 +1016,61 @@ describe("MatrixClient event timelines", function () { httpBackend.flushAllExpected(), ]); }); + + it("should create threads for thread roots discovered", function () { + const room = client.getRoom(roomId)!; + const timelineSet = room.getTimelineSets()[0]; + + httpBackend + .when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id!)) + .respond(200, function () { + return { + start: "start_token0", + events_before: [], + event: EVENTS[0], + events_after: [], + end: "end_token0", + state: [], + }; + }); + + httpBackend + .when("GET", "/rooms/!foo%3Abar/messages") + .check(function (req) { + const params = req.queryParams!; + expect(params.dir).toEqual("b"); + expect(params.from).toEqual("start_token0"); + expect(params.limit).toEqual("30"); + }) + .respond(200, function () { + return { + chunk: [EVENTS[1], EVENTS[2], THREAD_ROOT], + end: "start_token1", + }; + }); + + let tl: EventTimeline; + return Promise.all([ + client + .getEventTimeline(timelineSet, EVENTS[0].event_id!) + .then(function (tl0) { + tl = tl0!; + return client.paginateEventTimeline(tl, { backwards: true }); + }) + .then(function (success) { + expect(success).toBeTruthy(); + expect(tl!.getEvents().length).toEqual(4); + expect(tl!.getEvents()[0].event).toEqual(THREAD_ROOT); + expect(tl!.getEvents()[1].event).toEqual(EVENTS[2]); + expect(tl!.getEvents()[2].event).toEqual(EVENTS[1]); + expect(tl!.getEvents()[3].event).toEqual(EVENTS[0]); + expect(room.getThreads().map((it) => it.id)).toEqual([THREAD_ROOT.event_id!]); + expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("start_token1"); + expect(tl!.getPaginationToken(EventTimeline.FORWARDS)).toEqual("end_token0"); + }), + httpBackend.flushAllExpected(), + ]); + }); }); it("should ensure thread events are ordered correctly", async () => { diff --git a/src/client.ts b/src/client.ts index 832df1e1fc8..8066b22c8cd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -6059,7 +6059,7 @@ export class MatrixClient extends TypedEventEmitter it.isRelation(THREAD_RELATION_TYPE.name)), + timelineEvents.filter((it) => it.getServerAggregatedRelation(THREAD_RELATION_TYPE.name)), false, ); From 3872c5f0992cca6a1ff6e37dc86d6112b72ed0fb Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Mon, 19 Dec 2022 11:54:20 +0100 Subject: [PATCH 26/88] Update src/sliding-sync.ts Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/sliding-sync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 7287489d30e..3a2f6739135 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -397,7 +397,7 @@ export class SlidingSync extends TypedEventEmitter Date: Mon, 19 Dec 2022 14:53:08 +0100 Subject: [PATCH 27/88] Don't use `RoomMember` as a `calls` a key on `GroupCall` (#2993) --- spec/test-utils/webrtc.ts | 3 - spec/unit/webrtc/groupCall.spec.ts | 125 ++++++++++++++++++++++++----- src/webrtc/groupCall.ts | 70 ++++++++-------- 3 files changed, 142 insertions(+), 56 deletions(-) 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/webrtc/groupCall.spec.ts b/spec/unit/webrtc/groupCall.spec.ts index 59cdbac129d..281497bbf62 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); @@ -839,7 +855,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 +882,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 +959,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 +974,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 +987,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 +1128,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/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index 34a35e5a686..f1162447ade 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -55,7 +55,7 @@ export enum GroupCallEvent { export type GroupCallEventHandlerMap = { [GroupCallEvent.GroupCallStateChanged]: (newState: GroupCallState, oldState: GroupCallState) => 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; @@ -728,18 +728,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 +747,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 +775,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 +819,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 +828,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 +841,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 +865,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 +875,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 +1020,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); From b83c3728486a4d3c7f9d660c4cd0f678e73746c7 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 20 Dec 2022 09:22:26 +0100 Subject: [PATCH 28/88] Implement MSC3912: Relation-based redactions (#2954) --- spec/unit/matrix-client.spec.ts | 63 +++++++++++++++++++++++++++++++-- src/@types/event.ts | 9 +++++ src/@types/requests.ts | 14 +++++++- src/client.ts | 29 +++++++++++++-- src/feature.ts | 4 +++ 5 files changed, 113 insertions(+), 6 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 68c6ded9fda..58e6a579990 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"; @@ -121,6 +123,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; @@ -132,9 +138,7 @@ describe("MatrixClient", function () { function httpReq(method: Method, path: string, qp?: QueryDict, data?: BodyInit, opts?: IRequestOpts) { 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"], }); } @@ -1085,6 +1089,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", () => { diff --git a/src/@types/event.ts b/src/@types/event.ts index 230cb05039d..4d7cd5d510a 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -165,6 +165,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/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/client.ts b/src/client.ts index 8066b22c8cd..26d31ef0c86 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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"; @@ -4444,9 +4445,11 @@ export class MatrixClient extends TypedEventEmitter = { [Feature.LoginTokenRequest]: { unstablePrefixes: ["org.matrix.msc3882"], }, + [Feature.RelationBasedRedactions]: { + unstablePrefixes: ["org.matrix.msc3912"], + }, [Feature.AccountDataDeletion]: { unstablePrefixes: ["org.matrix.msc3391"], }, From 45f6c5b07935027b80ef3ac216fada05bb50b11b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 20 Dec 2022 11:11:00 +0000 Subject: [PATCH 29/88] Add `exportRoomKeys` to `CryptoBackend` (#2970) Element-web calls `exportRoomKeys` on logout, so we need a stub implementation to get it EW working with the rust crypto sdk. --- spec/unit/rust-crypto.spec.ts | 46 ++++++++++++++++++++++++++++++ src/@types/crypto.ts | 26 +++++++++++++++++ src/client.ts | 6 ++-- src/common-crypto/CryptoBackend.ts | 12 +++++++- src/crypto/OlmDevice.ts | 3 +- src/crypto/algorithms/base.ts | 3 +- src/crypto/algorithms/megolm.ts | 4 +-- src/crypto/backup.ts | 3 +- src/crypto/index.ts | 26 ++--------------- src/rust-crypto/rust-crypto.ts | 7 ++++- 10 files changed, 102 insertions(+), 34 deletions(-) create mode 100644 spec/unit/rust-crypto.spec.ts diff --git a/spec/unit/rust-crypto.spec.ts b/spec/unit/rust-crypto.spec.ts new file mode 100644 index 00000000000..81128d9d025 --- /dev/null +++ b/spec/unit/rust-crypto.spec.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 "fake-indexeddb/auto"; +import { IDBFactory } from "fake-indexeddb"; + +import { RustCrypto } from "../../src/rust-crypto/rust-crypto"; +import { initRustCrypto } from "../../src/rust-crypto"; + +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"; + + let rustCrypto: RustCrypto; + + beforeEach(async () => { + rustCrypto = (await initRustCrypto(TEST_USER, TEST_DEVICE_ID)) as RustCrypto; + }); + + describe(".exportRoomKeys", () => { + it("should return a list", async () => { + const keys = await rustCrypto.exportRoomKeys(); + expect(Array.isArray(keys)).toBeTruthy(); + }); + }); +}); 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/client.ts b/src/client.ts index 26d31ef0c86..343d5afe590 100644 --- a/src/client.ts +++ b/src/client.ts @@ -20,6 +20,7 @@ limitations under the License. import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk"; +import type { IMegolmSessionData } from "./@types/crypto"; import { ISyncStateData, SyncApi, SyncState } from "./sync"; import { EventStatus, @@ -74,7 +75,6 @@ import { ICryptoCallbacks, IBootstrapCrossSigningOpts, ICheckOwnCrossSigningTrustOpts, - IMegolmSessionData, isCryptoAvailable, VerificationMethod, IRoomKeyRequestBody, @@ -3034,10 +3034,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(); } /** diff --git a/src/common-crypto/CryptoBackend.ts b/src/common-crypto/CryptoBackend.ts index 77748cb5050..2d09fc22721 100644 --- a/src/common-crypto/CryptoBackend.ts +++ b/src/common-crypto/CryptoBackend.ts @@ -14,7 +14,7 @@ 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 { MatrixEvent } from "../models/event"; /** @@ -60,4 +60,14 @@ 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; } 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..91eb9906277 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"; @@ -171,26 +171,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 { @@ -3894,5 +3874,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/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 07b7ea3b9d9..a273bd16428 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -16,7 +16,7 @@ limitations under the License. import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; -import { IEventDecryptionResult } from "../@types/crypto"; +import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto"; import { MatrixEvent } from "../models/event"; import { CryptoBackend } from "../common-crypto/CryptoBackend"; @@ -57,4 +57,9 @@ export class RustCrypto implements CryptoBackend { // TODO return false; } + + public async exportRoomKeys(): Promise { + // TODO + return []; + } } From ec2405ac99fd1734b3fd074c030656c14a8677ee Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 21 Dec 2022 09:31:16 +0000 Subject: [PATCH 30/88] Fix release scripts to not fight with prettier (#2999) --- release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.sh b/release.sh index 92a593e180b..ae23183841b 100755 --- a/release.sh +++ b/release.sh @@ -184,7 +184,7 @@ for i in main typings do lib_value=$(jq -r ".matrix_lib_$i" package.json) if [ "$lib_value" != "null" ]; then - jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json + jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && prettier --write package.json fi done From 51a4cc5e1841227be6f865827705a6c69b6d0ece Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 21 Dec 2022 16:49:11 +0000 Subject: [PATCH 31/88] Fix post-release script compatibility with prettier --- post-release.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 1ebcac37cca04f108548376f02a7a2d0666904d6 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 21 Dec 2022 16:49:19 +0000 Subject: [PATCH 32/88] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index cc00a82f93c..5e6d6574c09 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "keywords": [ "matrix-org" ], - "main": "./lib/index.js", + "main": "./src/index.ts", "browser": "./lib/browser-index.ts", "matrix_src_main": "./src/index.ts", "matrix_src_browser": "./src/browser-index.ts", @@ -143,6 +143,5 @@ "outputDirectory": "coverage", "outputName": "jest-sonar-report.xml", "relativePaths": true - }, - "typings": "./lib/index.d.ts" + } } From af9525ed5fc43240e0dcdb35cc3b529355e2e9f6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Dec 2022 16:46:10 -0700 Subject: [PATCH 33/88] Add `device_id` to `/account/whoami` types (#3005) * Add `device_id` to `/account/whoami` types https://spec.matrix.org/v1.5/client-server-api/#get_matrixclientv3accountwhoami * Appease the linter * Modernize area of code * Remove unused eslint disable comment --- src/client.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index 343d5afe590..91b150c8cb0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -842,6 +842,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 @@ -9375,10 +9380,9 @@ export class MatrixClient extends TypedEventEmitter { - // eslint-disable-line camelcase + public async whoami(): Promise { return this.http.authedRequest(Method.Get, "/account/whoami"); } From aead401005fa9ecb9114b595d0eabfe5fb09116d Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 22 Dec 2022 08:54:55 +0000 Subject: [PATCH 34/88] Apply edits discovered from sync after thread is initialised (#3002) --- spec/unit/event-timeline-set.spec.ts | 87 +++++++++++++++++++++++++++- src/models/thread.ts | 23 ++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) 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/src/models/thread.ts b/src/models/thread.ts index bca2d92b427..da8ddf0b1f8 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -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(); @@ -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); @@ -375,6 +394,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); From 21e66a5c34da35908454c3fe4f79fbd8680652ff Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 22 Dec 2022 14:52:16 +0000 Subject: [PATCH 35/88] bugfix: upload OTKs in sliding sync mode --- src/sliding-sync-sdk.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index c6d5f98a518..db8a0b93fe4 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -102,6 +102,7 @@ class ExtensionE2EE implements Extension Date: Fri, 23 Dec 2022 11:03:14 +0000 Subject: [PATCH 36/88] Break coupling between `Room` and `Crypto.trackRoomDevices` (#2998) `Room` and `Crypto` currently have some tight coupling in the form of a call to `trackRoomDevices` when out-of-band members are loaded. We can improve this by instead having Crypto listen out for a `RoomSateEvent.Update` notification. --- spec/TestClient.ts | 11 +++-- spec/integ/megolm-integ.spec.ts | 88 +++++++++++++++++++++++++++++++++ src/crypto/index.ts | 22 +++++++-- src/models/room-state.ts | 10 ++++ src/models/room.ts | 18 +++++-- 5 files changed, 137 insertions(+), 12 deletions(-) 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/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/src/crypto/index.ts b/src/crypto/index.ts index 91eb9906277..b925483e427 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -89,6 +89,7 @@ import { ISyncResponse } from "../sync-accumulator"; import { ISignatures } from "../@types/signed"; import { IMessage } from "./algorithms/olm"; import { CryptoBackend } from "../common-crypto/CryptoBackend"; +import { RoomState, RoomStateEvent } from "../models/room-state"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -2606,14 +2607,23 @@ 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); } } @@ -2622,6 +2632,8 @@ export class Crypto extends TypedEventEmitter { const room = this.clientStore.getRoom(roomId); diff --git a/src/models/room-state.ts b/src/models/room-state.ts index 3940659f24c..a17d92b568d 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -623,6 +623,16 @@ export class RoomState 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..0faea02c371 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -888,6 +888,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 +923,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) => { From 0f717a9306e9c48fcb3037ba3af290fa77cb357c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 3 Jan 2023 00:58:55 -0700 Subject: [PATCH 37/88] Update comment for `m.key.verification.ready` event type definition (#3006) --- src/@types/event.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/@types/event.ts b/src/@types/event.ts index 4d7cd5d510a..f92cd31af24 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -61,7 +61,7 @@ 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", From 7d37bb1edb156cecd255fe5f57c5718d2b621487 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 3 Jan 2023 00:59:12 -0700 Subject: [PATCH 38/88] Remove usage of v1 Identity Server API (#3003) * Remove usage of v1 Identity Server API It's been deprecated for over a year at this point - everyone should be able to support v2. * Missed one. --- spec/unit/autodiscovery.spec.ts | 12 ++++++------ src/autodiscovery.ts | 4 ++-- src/http-api/prefix.ts | 5 ----- 3 files changed, 8 insertions(+), 13 deletions(-) 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/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/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 */ From 3b66b28e714e517aad49c7a39ba4cf26e337ab9c Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 3 Jan 2023 10:21:40 +0000 Subject: [PATCH 39/88] In release.yml use an env section to get ref_name --- .github/workflows/release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 609eb38429f..bf40732144a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,8 +32,10 @@ jobs: ref: gh-pages - name: 🔪 Prepare + env: + GITHUB_REF_NAME: ${{ github.ref_name }} run: | - tag="${{ github.ref_name }}" + tag="${GITHUB_REF_NAME}" VERSION="${tag#v}" [ ! -e "$VERSION" ] || rm -r $VERSION cp -r $RUNNER_TEMP/_docs/ $VERSION From 33c9af952e6d351424075bb63e4a4400d026e338 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 3 Jan 2023 10:25:59 +0000 Subject: [PATCH 40/88] Reformat release.yml --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf40732144a..f1c661277c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: - name: 🔪 Prepare env: - GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_REF_NAME: ${{ github.ref_name }} run: | tag="${GITHUB_REF_NAME}" VERSION="${tag#v}" From fcb75d547eef65930760b651806e825ac1d3f40e Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 3 Jan 2023 12:25:13 +0000 Subject: [PATCH 41/88] Remove unneeded variable in workflow --- .github/workflows/release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1c661277c6..962e2b36c9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,8 +35,7 @@ jobs: env: GITHUB_REF_NAME: ${{ github.ref_name }} run: | - tag="${GITHUB_REF_NAME}" - VERSION="${tag#v}" + VERSION="${GITHUB_REF_NAME#v}" [ ! -e "$VERSION" ] || rm -r $VERSION cp -r $RUNNER_TEMP/_docs/ $VERSION From e9fef19c8f8cfcdebe2e1680cfbcd9c7904b7b48 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Jan 2023 13:28:02 +0000 Subject: [PATCH 42/88] Update dependency @types/node to v18.11.18 (#2984) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 232e98e6ced..79c83183742 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1754,9 +1754,9 @@ integrity sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw== "@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" @@ -1764,9 +1764,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" @@ -1809,9 +1809,9 @@ integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^17.0.8": - version "17.0.17" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.17.tgz#5672e5621f8e0fca13f433a8017aae4b7a2a03e7" - integrity sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g== + version "17.0.18" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.18.tgz#466225ab4fbabb9aa711f5b406796daf1374a5b7" + integrity sha512-eIJR1UER6ur3EpKM3d+2Pgd+ET+k6Kn9B4ZItX0oPjjVI5PrfaRjKyLT5UYendDpLuoiJMNJvovLQbEXqhsPaw== dependencies: "@types/yargs-parser" "*" From 9b372d23ca0e09ba1d778331bed30c5d9fd5bf60 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 3 Jan 2023 13:38:21 +0000 Subject: [PATCH 43/88] Factor `SyncApi` options out of `IStoredClientOptions` (#3009) There are a couple of callback interfaces which are currently stuffed into `IStoredClientOpts` to make it easier to pass them into the `SyncApi` constructor. Before we add more fields to this, let's separate it out to a separate object. --- spec/integ/matrix-client-methods.spec.ts | 2 - spec/integ/sliding-sync-sdk.spec.ts | 7 +- src/client.ts | 55 +++++++------- src/embedded.ts | 4 +- src/sliding-sync-sdk.ts | 39 +++++----- src/sync.ts | 91 ++++++++++++++++-------- 6 files changed, 116 insertions(+), 82 deletions(-) diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index c83ada2d597..c1c4c30dc5d 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(); diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 165e3142012..499bf08c6a3 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.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/src/client.ts b/src/client.ts index 91b150c8cb0..278237d15f4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -21,7 +21,7 @@ limitations under the License. import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk"; import type { IMegolmSessionData } from "./@types/crypto"; -import { ISyncStateData, SyncApi, SyncState } from "./sync"; +import { ISyncStateData, SyncApi, SyncApiOptions, SyncState } from "./sync"; import { EventStatus, IContent, @@ -456,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", @@ -1433,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) { @@ -1459,6 +1447,21 @@ 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. @@ -3954,7 +3957,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 @@ -6154,7 +6157,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); } @@ -6661,7 +6664,7 @@ export class MatrixClient extends TypedEventEmitter = {}, + 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]); @@ -377,8 +378,8 @@ export class SlidingSyncSdk { 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); @@ -698,7 +699,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 @@ -730,8 +731,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/sync.ts b/src/sync.ts index 0169ff51186..d2576b32a9c 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -34,7 +34,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, @@ -59,6 +59,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 +111,22 @@ function debuglog(...params: any[]): void { logger.log(...params); } +/** + * Options passed into the constructor of SyncApi by MatrixClient + */ +export interface SyncApiOptions { + // 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; +} + interface ISyncOptions { filter?: string; hasSyncedBefore?: boolean; @@ -163,7 +180,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 +219,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 +663,7 @@ export class SyncApi { return; } if (this.opts.lazyLoadMembers) { - this.opts.crypto?.enableLazyLoading(); + this.syncOpts.crypto?.enableLazyLoading(); } try { debuglog("Storing client options..."); @@ -866,10 +897,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 +925,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.crypto) { + await this.syncOpts.crypto.onSyncCompleted(syncEventData); } // keep emitting SYNCING -> SYNCING for clients who want to do bulk updates @@ -907,8 +938,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. @@ -1356,7 +1387,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 +1401,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 +1493,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 +1503,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 +1516,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"), ); } From cef5507ab1f0464ff150f2500c7b8befe33ec392 Mon Sep 17 00:00:00 2001 From: Ankit <61223051+ankit-pn@users.noreply.github.com> Date: Tue, 3 Jan 2023 19:37:37 +0530 Subject: [PATCH 44/88] Include 'yarn install' . (#3011) --- examples/browser/README.md | 1 + 1 file changed, 1 insertion(+) 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 From 7c34deecb6b4d806ad1c37abeee3f44e1f67e2e3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 3 Jan 2023 15:37:51 +0000 Subject: [PATCH 45/88] Pass CryptoBackend into SyncApi (#3010) I need to start calling back into the new rust crypto implementation from the /sync loops, so I need to pass it into SyncApi. To reduce the coupling, I've defined a new interface specifying the methods which exist for that purpose. Currently it's only onSyncCompleted. --- spec/integ/sliding-sync-sdk.spec.ts | 2 +- src/client.ts | 1 + src/common-crypto/CryptoBackend.ts | 26 +++++++++++++++++++++++++- src/crypto/index.ts | 4 ++-- src/rust-crypto/rust-crypto.ts | 10 +++++++++- src/sync.ts | 16 +++++++++++++--- 6 files changed, 51 insertions(+), 8 deletions(-) diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 499bf08c6a3..a54cf71cb14 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -119,7 +119,7 @@ describe("SlidingSyncSdk", () => { if (testOpts.withCrypto) { httpBackend!.when("GET", "/room_keys/version").respond(404, {}); await client!.initCrypto(); - syncOpts.crypto = client!.crypto; + syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto; } httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {}); sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts); diff --git a/src/client.ts b/src/client.ts index 278237d15f4..3430c008eb2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1453,6 +1453,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.canResetTimelineCallback) { return false; diff --git a/src/common-crypto/CryptoBackend.ts b/src/common-crypto/CryptoBackend.ts index 2d09fc22721..82db3f28a9c 100644 --- a/src/common-crypto/CryptoBackend.ts +++ b/src/common-crypto/CryptoBackend.ts @@ -20,7 +20,7 @@ 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 @@ -71,3 +71,27 @@ export interface CryptoBackend { */ exportRoomKeys(): Promise; } + +/** The methods which crypto implementations should expose to the Sync api */ +export interface SyncCryptoCallbacks { + /** + * 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/index.ts b/src/crypto/index.ts index b925483e427..fe8719a4c4a 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -88,7 +88,7 @@ import { IContent } from "../models/event"; import { ISyncResponse } 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; @@ -3013,7 +3013,7 @@ export class Crypto extends TypedEventEmitter { + public async onSyncCompleted(syncData: OnSyncCompletedData): Promise { this.deviceList.setSyncToken(syncData.nextSyncToken ?? null); this.deviceList.saveIfDirty(); diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index a273bd16428..b28b8b858c8 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -18,7 +18,7 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto"; import { MatrixEvent } from "../models/event"; -import { CryptoBackend } from "../common-crypto/CryptoBackend"; +import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; // import { logger } from "../logger"; @@ -62,4 +62,12 @@ export class RustCrypto implements CryptoBackend { // TODO return []; } + + /** 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 {} } diff --git a/src/sync.ts b/src/sync.ts index d2576b32a9c..0ba52ba8c0c 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"; @@ -115,9 +116,18 @@ function debuglog(...params: any[]): void { * Options passed into the constructor of SyncApi by MatrixClient */ export interface SyncApiOptions { - // Crypto manager + /** + * 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 @@ -925,8 +935,8 @@ export class SyncApi { // tell the crypto module to do its processing. It may block (to do a // /keys/changes request). - if (this.syncOpts.crypto) { - await this.syncOpts.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 From 6168cedf32e8a4c71c9ebc8c36bd33dd62a62c01 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 3 Jan 2023 11:06:54 -0500 Subject: [PATCH 46/88] Avoid triggering decryption errors when decrypting redacted events (#3004) --- spec/unit/crypto.spec.ts | 32 ++++++++++++++++++++++++++++++++ src/crypto/index.ts | 15 +++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) 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/src/crypto/index.ts b/src/crypto/index.ts index fe8719a4c4a..24b36b08143 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -2878,11 +2878,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: { @@ -2890,7 +2901,7 @@ export class Crypto extends TypedEventEmitter Date: Wed, 4 Jan 2023 12:17:42 +0000 Subject: [PATCH 47/88] Handle outgoing requests from rust crypto SDK (#3019) The rust matrix-sdk-crypto has an `outgoingRequests()` method which we need to poll, and make the requested requests. --- spec/unit/rust-crypto.spec.ts | 169 ++++++++++++++++++++++++++++++++- src/client.ts | 2 +- src/rust-crypto/index.ts | 9 +- src/rust-crypto/rust-crypto.ts | 116 +++++++++++++++++++++- 4 files changed, 284 insertions(+), 12 deletions(-) diff --git a/spec/unit/rust-crypto.spec.ts b/spec/unit/rust-crypto.spec.ts index 81128d9d025..822c08bf6c9 100644 --- a/spec/unit/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto.spec.ts @@ -16,9 +16,21 @@ 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, + 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, IHttpOpts, 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 @@ -31,16 +43,163 @@ describe("RustCrypto", () => { const TEST_USER = "@alice:example.com"; const TEST_DEVICE_ID = "TEST_DEVICE"; - let rustCrypto: RustCrypto; + describe(".exportRoomKeys", () => { + let rustCrypto: RustCrypto; - beforeEach(async () => { - rustCrypto = (await initRustCrypto(TEST_USER, TEST_DEVICE_ID)) as RustCrypto; - }); + beforeEach(async () => { + const mockHttpApi = {} as MatrixHttpApi; + rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto; + }); - describe(".exportRoomKeys", () => { it("should return a list", async () => { const keys = await rustCrypto.exportRoomKeys(); expect(Array.isArray(keys)).toBeTruthy(); }); }); + + 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, + }); + + // 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/src/client.ts b/src/client.ts index 3430c008eb2..ae42103f26e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2148,7 +2148,7 @@ export class MatrixClient extends TypedEventEmitter { +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(); @@ -34,7 +39,7 @@ export async function initRustCrypto(userId: string, deviceId: string): Promise< // 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, userId, deviceId); + 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 index b28b8b858c8..c6ff569a816 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -15,12 +15,28 @@ 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 { MatrixEvent } from "../models/event"; import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; +import { logger } from "../logger"; +import { IHttpOpts, IRequestOpts, MatrixHttpApi, Method } from "../http-api"; +import { QueryDict } from "../utils"; -// import { logger } from "../logger"; +/** + * 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. @@ -29,10 +45,18 @@ export class RustCrypto implements CryptoBackend { public globalBlacklistUnverifiedDevices = false; public globalErrorOnUnknownDevices = false; - /** whether stop() has been called */ + /** whether {@link stop} has been called */ private stopped = false; - public constructor(private readonly olmMachine: RustSdkCryptoJs.OlmMachine, _userId: string, _deviceId: string) {} + /** 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 @@ -63,11 +87,95 @@ export class RustCrypto implements CryptoBackend { return []; } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // SyncCryptoCallbacks implementation + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /** 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 {} + 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, + opts: IRequestOpts = {}, + ): Promise { + // unbeknownst to HttpApi, we are sending JSON + opts.headers ??= {}; + opts.headers["Content-Type"] = "application/json"; + + // we use the full prefix + opts.prefix ??= ""; + + const resp = await this.http.authedRequest(method, path, queryParams, body, opts); + return await resp.text(); + } } From 64119ef9158b87f085b2365a7bda52df566f0043 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 4 Jan 2023 14:17:55 +0000 Subject: [PATCH 48/88] Avoid logical assignment operator (#3022) Apparently `??=` was only added to javascript in ES12, and our eleweb build doesn't support it. Fixes breakage introduced in #3019. --- src/rust-crypto/rust-crypto.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index c6ff569a816..7231899b52c 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -169,11 +169,11 @@ export class RustCrypto implements CryptoBackend { opts: IRequestOpts = {}, ): Promise { // unbeknownst to HttpApi, we are sending JSON - opts.headers ??= {}; + if (!opts.headers) opts.headers = {}; opts.headers["Content-Type"] = "application/json"; // we use the full prefix - opts.prefix ??= ""; + if (!opts.prefix) opts.prefix = ""; const resp = await this.http.authedRequest(method, path, queryParams, body, opts); return await resp.text(); From 9ca3e7272e73305757a2648b45cb9d133cc5989d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Jan 2023 13:44:26 -0700 Subject: [PATCH 49/88] chore(deps): lock file maintenance (#3014) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 849 ++++++++++++++++++++++++++---------------------------- 1 file changed, 404 insertions(+), 445 deletions(-) diff --git a/yarn.lock b/yarn.lock index 79c83183742..0f09a00c784 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== +"@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.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== - 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" @@ -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,9 +1376,9 @@ "@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" @@ -1439,6 +1389,7 @@ "@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": @@ -1608,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" @@ -1658,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" @@ -1722,9 +1673,9 @@ "@types/istanbul-lib-report" "*" "@types/jest@^29.0.0": - version "29.2.4" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.4.tgz#9c155c4b81c9570dbd183eb8604aa0ae80ba5a5b" - integrity sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A== + 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" @@ -1748,12 +1699,7 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/node@*": - version "18.11.15" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.15.tgz#de0e1fbd2b22b962d45971431e2ae696643d3f5d" - integrity sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw== - -"@types/node@18": +"@types/node@*", "@types/node@18": version "18.11.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== @@ -1809,20 +1755,20 @@ integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^17.0.8": - version "17.0.18" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.18.tgz#466225ab4fbabb9aa711f5b406796daf1374a5b7" - integrity sha512-eIJR1UER6ur3EpKM3d+2Pgd+ET+k6Kn9B4ZItX0oPjjVI5PrfaRjKyLT5UYendDpLuoiJMNJvovLQbEXqhsPaw== + 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.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz#098abb4c9354e19f460d57ab18bff1f676a6cff0" - integrity sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA== + 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.46.1" - "@typescript-eslint/type-utils" "5.46.1" - "@typescript-eslint/utils" "5.46.1" + "@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" @@ -1831,71 +1777,71 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.45.0": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.46.1.tgz#1fc8e7102c1141eb64276c3b89d70da8c0ba5699" - integrity sha512-RelQ5cGypPh4ySAtfIMBzBGyrNerQcmfA1oJvPj5f+H4jI59rl9xxpn4bonC0tQvUKOEN7eGBFWxFLK3Xepneg== + 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.46.1" - "@typescript-eslint/types" "5.46.1" - "@typescript-eslint/typescript-estree" "5.46.1" + "@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.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz#70af8425c79bbc1178b5a63fb51102ddf48e104a" - integrity sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA== +"@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.46.1" - "@typescript-eslint/visitor-keys" "5.46.1" + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/visitor-keys" "5.48.0" -"@typescript-eslint/type-utils@5.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz#195033e4b30b51b870dfcf2828e88d57b04a11cc" - integrity sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng== +"@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.46.1" - "@typescript-eslint/utils" "5.46.1" + "@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.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.46.1.tgz#4e9db2107b9a88441c4d5ecacde3bb7a5ebbd47e" - integrity sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w== +"@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.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz#5358088f98a8f9939355e0996f9c8f41c25eced2" - integrity sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg== +"@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.46.1" - "@typescript-eslint/visitor-keys" "5.46.1" + "@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.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.46.1.tgz#7da3c934d9fd0eb4002a6bb3429f33298b469b4a" - integrity sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA== +"@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.46.1" - "@typescript-eslint/types" "5.46.1" - "@typescript-eslint/typescript-estree" "5.46.1" + "@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.46.1": - version "5.46.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz#126cc6fe3c0f83608b2b125c5d9daced61394242" - integrity sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg== +"@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.46.1" + "@typescript-eslint/types" "5.48.0" eslint-visitor-keys "^3.3.0" JSONStream@^1.0.3: @@ -1912,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" @@ -1970,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== @@ -2057,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" @@ -2590,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" @@ -2647,9 +2593,9 @@ chokidar@^3.4.0: fsevents "~2.3.2" ci-info@^3.2.0, 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== + 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" @@ -2852,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" @@ -2995,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" @@ -3187,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" @@ -3207,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" @@ -3217,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" @@ -3232,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: @@ -3326,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" @@ -3679,9 +3626,9 @@ fast-safe-stringify@^2.0.7: integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== fastq@^1.6.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.14.0.tgz#107f69d7295b11e0fccc264e1fc6389f623731ce" - integrity sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg== + 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" @@ -3825,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== @@ -3853,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" @@ -3888,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" @@ -3913,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" @@ -4084,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" @@ -4156,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" @@ -4618,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" @@ -4875,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" @@ -4910,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" @@ -4933,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: @@ -4972,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" @@ -5134,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" @@ -5175,10 +5129,10 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -marked@^4.0.19: - version "4.2.4" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.4.tgz#5a4ce6c7a1ae0c952601fce46376ee4cf1797e1c" - integrity sha512-Wcc9ikX7Q5E4BYDPvh1C6QNSxrjC9tBgz+A/vAhp59KXUgachw++uMvMKiSW8oA85nopmPZcEvBoex/YLMsiyA== +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" @@ -5288,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.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.1.tgz#6c9dffcf9927ff2a31e74b5af11adf8b9604b022" - integrity sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g== +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" @@ -5332,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" @@ -5394,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" @@ -5607,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" @@ -5988,9 +5942,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" @@ -6098,10 +6052,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" @@ -6124,17 +6078,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" @@ -6503,7 +6457,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== @@ -6512,7 +6466,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== @@ -6884,13 +6838,13 @@ typedoc-plugin-missing-exports@^1.0.0: integrity sha512-7s6znXnuAj1eD9KYPyzVzR1lBF5nwAY8IKccP5sdoO9crG4lpd16RoFpLsh2PccJM+I2NASpr0+/NMka6ThwVA== typedoc@^0.23.20: - version "0.23.22" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.23.22.tgz#e25281ca816cd92ecfdaf3ec336d27e7bebb69ac" - integrity sha512-5sJkjK60xp8A7YpcYniu3+Wf0QcgojEnhzHuCN+CkdpQkKRhOspon/9+sGTkGI8kjVkZs3KHrhltpQyVhRMVfw== + 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: @@ -6981,10 +6935,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" @@ -7119,9 +7073,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" @@ -7133,10 +7087,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" @@ -7279,7 +7233,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== @@ -7314,6 +7268,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" From 22f10f71b8c505ac6e8e542f62d5b68c0b093d04 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 5 Jan 2023 09:54:44 +0000 Subject: [PATCH 50/88] indirect decryption attempts via Client (#3023) ... to reduce the number of things referring to `client.crypto` --- src/models/room.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/models/room.ts b/src/models/room.ts index 0faea02c371..ad21e626450 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -441,9 +441,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 +501,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 +518,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); } From 030abe156350deaa05eb421566a40bd0924f7c03 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 5 Jan 2023 09:54:56 +0000 Subject: [PATCH 51/88] Pass to-device messages into rust crypto-sdk (#3021) We need a separate API, because `ClientEvent.ToDeviceEvent` is only emitted for successfully decrypted to-device events --- spec/unit/rust-crypto.spec.ts | 44 +++++++++++++++++++++++++++++- src/common-crypto/CryptoBackend.ts | 15 ++++++++++ src/crypto/index.ts | 17 +++++++++++- src/event-mapper.ts | 3 ++ src/rust-crypto/rust-crypto.ts | 20 ++++++++++++++ src/sliding-sync-sdk.ts | 13 ++++++--- src/sync.ts | 21 ++++++-------- 7 files changed, 115 insertions(+), 18 deletions(-) diff --git a/spec/unit/rust-crypto.spec.ts b/spec/unit/rust-crypto.spec.ts index 822c08bf6c9..0bcc88d5d26 100644 --- a/spec/unit/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto.spec.ts @@ -22,6 +22,7 @@ import { KeysClaimRequest, KeysQueryRequest, KeysUploadRequest, + OlmMachine, SignatureUploadRequest, } from "@matrix-org/matrix-sdk-crypto-js"; import { Mocked } from "jest-mock"; @@ -29,7 +30,7 @@ import MockHttpBackend from "matrix-mock-request"; import { RustCrypto } from "../../src/rust-crypto/rust-crypto"; import { initRustCrypto } from "../../src/rust-crypto"; -import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, MatrixHttpApi } from "../../src"; +import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IToDeviceEvent, MatrixHttpApi } from "../../src"; import { TypedEventEmitter } from "../../src/models/typed-event-emitter"; afterEach(() => { @@ -57,6 +58,47 @@ describe("RustCrypto", () => { }); }); + describe("to-device messages", () => { + let rustCrypto: RustCrypto; + + beforeEach(async () => { + const mockHttpApi = {} as MatrixHttpApi; + 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; diff --git a/src/common-crypto/CryptoBackend.ts b/src/common-crypto/CryptoBackend.ts index 82db3f28a9c..a90afdf3a82 100644 --- a/src/common-crypto/CryptoBackend.ts +++ b/src/common-crypto/CryptoBackend.ts @@ -15,6 +15,7 @@ limitations under the License. */ import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto"; +import type { IToDeviceEvent } from "../sync-accumulator"; import { MatrixEvent } from "../models/event"; /** @@ -74,6 +75,20 @@ export interface CryptoBackend extends SyncCryptoCallbacks { /** 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. * diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 24b36b08143..458132e75bb 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -85,7 +85,7 @@ 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, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; @@ -3198,6 +3198,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( 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/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 7231899b52c..9bb75be70ee 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -24,6 +24,7 @@ import { } 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"; @@ -93,6 +94,25 @@ export class RustCrypto implements CryptoBackend { // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /** 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. diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 18c94c16836..91ff9d7a75e 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -14,6 +14,7 @@ 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"; @@ -127,7 +128,7 @@ type ExtensionToDeviceResponse = { class ExtensionToDevice 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"; @@ -150,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 @@ -373,7 +378,7 @@ export class SlidingSyncSdk { this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this)); this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this)); const extensions: Extension[] = [ - new ExtensionToDevice(this.client), + new ExtensionToDevice(this.client, this.syncOpts.cryptoCallbacks), new ExtensionAccountData(this.client), new ExtensionTyping(this.client), new ExtensionReceipts(this.client), diff --git a/src/sync.ts b/src/sync.ts index 0ba52ba8c0c..11fe3cc102b 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -48,6 +48,7 @@ import { IStrippedState, ISyncResponse, ITimeline, + IToDeviceEvent, } from "./sync-accumulator"; import { MatrixEvent } from "./models/event"; import { MatrixError, Method } from "./http-api"; @@ -1170,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 From 695b773f8bedea5f6b4072aef602ac2a057f3538 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Thu, 5 Jan 2023 15:27:09 +0100 Subject: [PATCH 52/88] Fix false key requests after verifying new device (#3029) --- .../setDeviceVerification.spec.ts | 56 ------------------- src/crypto/index.ts | 3 - 2 files changed, 59 deletions(-) delete mode 100644 spec/unit/crypto/verification/setDeviceVerification.spec.ts 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/src/crypto/index.ts b/src/crypto/index.ts index 458132e75bb..441d569c4ac 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -2225,9 +2225,6 @@ export class Crypto extends TypedEventEmitter Date: Thu, 5 Jan 2023 15:00:37 +0000 Subject: [PATCH 53/88] Fix outgoing messages for rust-crypto (#3025) It turns out that MatrixClient uses a `FetchHttpApi` instance with `opts.onlyData = true`, so it was returning the json-parsed response rather than the raw response. Change the way we call `authedRequest` so that we get the raw body back. --- spec/unit/rust-crypto.spec.ts | 7 ++++--- src/rust-crypto/index.ts | 2 +- src/rust-crypto/rust-crypto.ts | 36 +++++++++++++++++----------------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/spec/unit/rust-crypto.spec.ts b/spec/unit/rust-crypto.spec.ts index 0bcc88d5d26..69dde71239e 100644 --- a/spec/unit/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto.spec.ts @@ -30,7 +30,7 @@ import MockHttpBackend from "matrix-mock-request"; import { RustCrypto } from "../../src/rust-crypto/rust-crypto"; import { initRustCrypto } from "../../src/rust-crypto"; -import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IToDeviceEvent, MatrixHttpApi } from "../../src"; +import { HttpApiEvent, HttpApiEventHandlerMap, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../src"; import { TypedEventEmitter } from "../../src/models/typed-event-emitter"; afterEach(() => { @@ -48,7 +48,7 @@ describe("RustCrypto", () => { let rustCrypto: RustCrypto; beforeEach(async () => { - const mockHttpApi = {} as MatrixHttpApi; + const mockHttpApi = {} as MatrixClient["http"]; rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto; }); @@ -62,7 +62,7 @@ describe("RustCrypto", () => { let rustCrypto: RustCrypto; beforeEach(async () => { - const mockHttpApi = {} as MatrixHttpApi; + const mockHttpApi = {} as MatrixClient["http"]; rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto; }); @@ -132,6 +132,7 @@ describe("RustCrypto", () => { 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 diff --git a/src/rust-crypto/index.ts b/src/rust-crypto/index.ts index e3b3cb67cd8..4c826078f9f 100644 --- a/src/rust-crypto/index.ts +++ b/src/rust-crypto/index.ts @@ -23,7 +23,7 @@ import { RUST_SDK_STORE_PREFIX } from "./constants"; import { IHttpOpts, MatrixHttpApi } from "../http-api"; export async function initRustCrypto( - http: MatrixHttpApi, + http: MatrixHttpApi, userId: string, deviceId: string, ): Promise { diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 9bb75be70ee..b48de46970b 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -28,7 +28,7 @@ import type { IToDeviceEvent } from "../sync-accumulator"; import { MatrixEvent } from "../models/event"; import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; import { logger } from "../logger"; -import { IHttpOpts, IRequestOpts, MatrixHttpApi, Method } from "../http-api"; +import { IHttpOpts, MatrixHttpApi, Method } from "../http-api"; import { QueryDict } from "../utils"; /** @@ -54,7 +54,7 @@ export class RustCrypto implements CryptoBackend { public constructor( private readonly olmMachine: RustSdkCryptoJs.OlmMachine, - private readonly http: MatrixHttpApi, + private readonly http: MatrixHttpApi, _userId: string, _deviceId: string, ) {} @@ -181,21 +181,21 @@ export class RustCrypto implements CryptoBackend { } } - private async rawJsonRequest( - method: Method, - path: string, - queryParams: QueryDict, - body: string, - opts: IRequestOpts = {}, - ): Promise { - // unbeknownst to HttpApi, we are sending JSON - if (!opts.headers) opts.headers = {}; - opts.headers["Content-Type"] = "application/json"; - - // we use the full prefix - if (!opts.prefix) opts.prefix = ""; - - const resp = await this.http.authedRequest(method, path, queryParams, body, opts); - return await resp.text(); + 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); } } From d02559cf3c04e4863fdb3874ad0e159a0101d36d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:02:19 +0000 Subject: [PATCH 54/88] Make error handling in decryptionLoop more generic (#3024) Not everything is a `DecryptionError`, and there's no real reason that we should only do retries for `DecryptionError`s --- spec/unit/models/event.spec.ts | 32 +++++++++++++++----------------- src/models/event.ts | 21 ++++----------------- 2 files changed, 19 insertions(+), 34 deletions(-) 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/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 From bb23df942382cb3b061034b2105ae2e7c7ac0191 Mon Sep 17 00:00:00 2001 From: Kerry Date: Fri, 6 Jan 2023 09:00:12 +1300 Subject: [PATCH 55/88] Add alt event type matching in Relations model (#3018) * allow alt event types in relations model * remove unneccesary checks on remove relation * comment * assert on event emitted --- spec/unit/relations.spec.ts | 96 ++++++++++++++++++++++++++++++++++++- src/models/relations.ts | 21 +++----- 2 files changed, 101 insertions(+), 16 deletions(-) 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/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 Date: Fri, 6 Jan 2023 10:17:29 +0100 Subject: [PATCH 56/88] Make poll start event type available --- src/@types/event.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/@types/event.ts b/src/@types/event.ts index d8bb4cc3081..32f32e72bb1 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -66,6 +66,7 @@ export enum EventType { // 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", From 896f6227a08766556b28a2c53c3aef5f17b70c17 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 6 Jan 2023 10:27:35 +0000 Subject: [PATCH 57/88] Fix threaded cache receipt when event holds multiple receipts (#3026) --- spec/integ/matrix-client-syncing.spec.ts | 46 ++++++++++++++++++++++++ src/@types/read_receipts.ts | 8 +++++ src/models/room.ts | 12 +++++-- src/models/thread.ts | 18 +++------- 4 files changed, 68 insertions(+), 16 deletions(-) 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/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/models/room.ts b/src/models/room.ts index ad21e626450..972112fdfef 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -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,7 @@ export class Room extends ReadReceipt { private txnToEvent: Record = {}; // Pending in-flight requests { string: MatrixEvent } private notificationCounts: NotificationCount = {}; private readonly threadNotifications = new Map(); - public readonly cachedThreadReadReceipts = new Map(); + public readonly cachedThreadReadReceipts = new Map(); private readonly timelineSets: EventTimelineSet[]; public readonly threadsTimelineSets: EventTimelineSet[] = []; // any filtered timeline sets we're maintaining for this room @@ -2718,7 +2724,7 @@ 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 }, ]); } }); diff --git a/src/models/thread.ts b/src/models/thread.ts index da8ddf0b1f8..25e454c32e2 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -27,7 +27,7 @@ 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 { @@ -317,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); } } From ca98d9ff111dc5357d166dcb87f3db564e6ee802 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 6 Jan 2023 13:59:34 +0000 Subject: [PATCH 58/88] Tests for convertQueryDictToStringRecord --- spec/unit/matrix-client.spec.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 2b48b69c392..b03c9b7f376 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -101,6 +101,33 @@ type WrappedRoom = Room & { _state: Map; }; +describe("convertQueryDictToStringRecord", () => { + it("returns an empty map when dict is undefined", () => { + expect(convertQueryDictToStringRecord(undefined)).toEqual({}); + }); + + it("converts an empty QueryDict to an empty map", () => { + expect(convertQueryDictToStringRecord({})).toEqual({}); + }); + + it("converts a QueryDict of strings to the equivalent map", () => { + expect(convertQueryDictToStringRecord({ a: "b", c: "d" })).toEqual({ a: "b", c: "d" }); + }); + + it("converts the values of the supplied QueryDict to strings", () => { + expect(convertQueryDictToStringRecord({ arr: ["b", "c"], num: 45, boo: true, und: undefined })).toEqual({ + arr: "b,c", + num: "45", + boo: "true", + und: "undefined", + }); + }); + + it("produces sane URLSearchParams conversions", () => { + expect(new URLSearchParams(convertQueryDictToStringRecord({ a: "b", c: "d" })).toString()).toEqual("a=b&c=d"); + }); +}); + describe("MatrixClient", function () { const userId = "@alice:bar"; const identityServerUrl = "https://identity.server"; From b1566ee54056d373e76109ff4bc28df0debecd4c Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 6 Jan 2023 14:16:21 +0000 Subject: [PATCH 59/88] Switch to a Map for convertQueryDictToStringRecord --- spec/unit/matrix-client.spec.ts | 44 +++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index b03c9b7f376..ce073aa7a69 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -70,15 +70,12 @@ jest.mock("../../src/webrtc/call", () => ({ // Utility function to ease the transition from our QueryDict type to a string record // which we can use to stringify with URLSearchParams -function convertQueryDictToStringRecord(queryDict?: QueryDict): Record { +function convertQueryDictToStringRecord(queryDict?: QueryDict): Map { if (!queryDict) { - return {}; + return new Map(); } - return Object.entries(queryDict).reduce((resultant, [key, value]) => { - resultant[key] = String(value); - return resultant; - }, {} as Record); + return new Map(Object.entries(queryDict).map(([k, v]) => [k, String(v)])); } type HttpLookup = { @@ -103,28 +100,37 @@ type WrappedRoom = Room & { describe("convertQueryDictToStringRecord", () => { it("returns an empty map when dict is undefined", () => { - expect(convertQueryDictToStringRecord(undefined)).toEqual({}); + expect(convertQueryDictToStringRecord(undefined)).toEqual(new Map()); }); it("converts an empty QueryDict to an empty map", () => { - expect(convertQueryDictToStringRecord({})).toEqual({}); + expect(convertQueryDictToStringRecord({})).toEqual(new Map()); }); it("converts a QueryDict of strings to the equivalent map", () => { - expect(convertQueryDictToStringRecord({ a: "b", c: "d" })).toEqual({ a: "b", c: "d" }); + expect(convertQueryDictToStringRecord({ a: "b", c: "d" })).toEqual( + new Map([ + ["a", "b"], + ["c", "d"], + ]), + ); }); it("converts the values of the supplied QueryDict to strings", () => { - expect(convertQueryDictToStringRecord({ arr: ["b", "c"], num: 45, boo: true, und: undefined })).toEqual({ - arr: "b,c", - num: "45", - boo: "true", - und: "undefined", - }); + expect(convertQueryDictToStringRecord({ 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(convertQueryDictToStringRecord({ a: "b", c: "d" })).toString()).toEqual("a=b&c=d"); + expect(new URLSearchParams(Array.from(convertQueryDictToStringRecord({ a: "b", c: "d" }))).toString()).toEqual( + "a=b&c=d", + ); }); }); @@ -248,10 +254,12 @@ describe("MatrixClient", function () { return Promise.resolve(next.data); } - const receivedRequestQueryString = new URLSearchParams(convertQueryDictToStringRecord(queryParams)).toString(); + const receivedRequestQueryString = new URLSearchParams( + Array.from(convertQueryDictToStringRecord(queryParams)), + ).toString(); const receivedRequestDebugString = `${method} ${prefix}${path}${receivedRequestQueryString}`; const expectedQueryString = new URLSearchParams( - convertQueryDictToStringRecord(next.expectQueryParams), + Array.from(convertQueryDictToStringRecord(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. From d7442147b9a00c951e93bef44eee33a6f590b803 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 6 Jan 2023 14:21:35 +0000 Subject: [PATCH 60/88] Rename convertQueryDictToMap --- spec/unit/matrix-client.spec.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index ce073aa7a69..19c8f176c79 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -68,9 +68,9 @@ jest.mock("../../src/webrtc/call", () => ({ supportsMatrixCall: jest.fn(() => false), })); -// Utility function to ease the transition from our QueryDict type to a string record -// which we can use to stringify with URLSearchParams -function convertQueryDictToStringRecord(queryDict?: QueryDict): Map { +// 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(); } @@ -100,15 +100,15 @@ type WrappedRoom = Room & { describe("convertQueryDictToStringRecord", () => { it("returns an empty map when dict is undefined", () => { - expect(convertQueryDictToStringRecord(undefined)).toEqual(new Map()); + expect(convertQueryDictToMap(undefined)).toEqual(new Map()); }); it("converts an empty QueryDict to an empty map", () => { - expect(convertQueryDictToStringRecord({})).toEqual(new Map()); + expect(convertQueryDictToMap({})).toEqual(new Map()); }); it("converts a QueryDict of strings to the equivalent map", () => { - expect(convertQueryDictToStringRecord({ a: "b", c: "d" })).toEqual( + expect(convertQueryDictToMap({ a: "b", c: "d" })).toEqual( new Map([ ["a", "b"], ["c", "d"], @@ -117,7 +117,7 @@ describe("convertQueryDictToStringRecord", () => { }); it("converts the values of the supplied QueryDict to strings", () => { - expect(convertQueryDictToStringRecord({ arr: ["b", "c"], num: 45, boo: true, und: undefined })).toEqual( + expect(convertQueryDictToMap({ arr: ["b", "c"], num: 45, boo: true, und: undefined })).toEqual( new Map([ ["arr", "b,c"], ["num", "45"], @@ -128,7 +128,7 @@ describe("convertQueryDictToStringRecord", () => { }); it("produces sane URLSearchParams conversions", () => { - expect(new URLSearchParams(Array.from(convertQueryDictToStringRecord({ a: "b", c: "d" }))).toString()).toEqual( + expect(new URLSearchParams(Array.from(convertQueryDictToMap({ a: "b", c: "d" }))).toString()).toEqual( "a=b&c=d", ); }); @@ -255,11 +255,11 @@ describe("MatrixClient", function () { } const receivedRequestQueryString = new URLSearchParams( - Array.from(convertQueryDictToStringRecord(queryParams)), + Array.from(convertQueryDictToMap(queryParams)), ).toString(); const receivedRequestDebugString = `${method} ${prefix}${path}${receivedRequestQueryString}`; const expectedQueryString = new URLSearchParams( - Array.from(convertQueryDictToStringRecord(next.expectQueryParams)), + 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. From c4ca0b2e07ed991705b430ccf34e2fba3a5b7d31 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 6 Jan 2023 15:13:44 +0000 Subject: [PATCH 61/88] Refactor timestampToEvent tests --- spec/unit/matrix-client.spec.ts | 158 ++++++++++++++------------------ 1 file changed, 68 insertions(+), 90 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 19c8f176c79..9a2ae6c978a 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -356,118 +356,96 @@ describe("MatrixClient", function () { 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 () => { - httpLookups = [ + await assertRequestsMade([ { - method: "GET", - path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, data: { event_id: eventId }, - expectQueryParams: { - ts: "0", - dir: "f", - }, }, - ]; - - await client.timestampToEvent(roomId, 0, Direction.Forward); - - expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(1); - const [method, path, queryParams, , { prefix } = { prefix: undefined }] = mocked(client.http.authedRequest) - .mock.calls[0]; - expect(method).toStrictEqual("GET"); - expect(prefix).toStrictEqual(ClientPrefix.V1); - expect(path).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); - expect(queryParams).toStrictEqual({ - ts: "0", - dir: "f", - }); + ]); }); it("should fallback to unstable endpoint when no support for stable endpoint", async () => { - httpLookups = [ + await assertRequestsMade([ { - method: "GET", - path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, prefix: ClientPrefix.V1, error: { httpStatus: 404, errcode: "M_UNRECOGNIZED", }, - expectQueryParams: { - ts: "0", - dir: "f", - }, }, { - method: "GET", - path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, prefix: unstableMSC3030Prefix, data: { event_id: eventId }, - expectQueryParams: { - ts: "0", - dir: "f", - }, }, - ]; - - await client.timestampToEvent(roomId, 0, Direction.Forward); - - expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(2); - const [stableMethod, stablePath, stableQueryParams, , { prefix: stablePrefix } = { prefix: undefined }] = - mocked(client.http.authedRequest).mock.calls[0]; - expect(stableMethod).toStrictEqual("GET"); - expect(stablePrefix).toStrictEqual(ClientPrefix.V1); - expect(stablePath).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); - expect(stableQueryParams).toStrictEqual({ - ts: "0", - dir: "f", - }); - - const [ - unstableMethod, - unstablePath, - unstableQueryParams, - , - { prefix: unstablePrefix } = { prefix: undefined }, - ] = mocked(client.http.authedRequest).mock.calls[1]; - expect(unstableMethod).toStrictEqual("GET"); - expect(unstablePrefix).toStrictEqual(unstableMSC3030Prefix); - expect(unstablePath).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); - expect(unstableQueryParams).toStrictEqual({ - ts: "0", - dir: "f", - }); + ]); }); it("should not fallback to unstable endpoint when stable endpoint returns an error", async () => { - httpLookups = [ - { - method: "GET", - path: `/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`, - prefix: ClientPrefix.V1, - error: { - httpStatus: 500, - errcode: "Fake response error", - }, - expectQueryParams: { - ts: "0", - dir: "f", + await assertRequestsMade( + [ + { + prefix: ClientPrefix.V1, + error: { + httpStatus: 500, + errcode: "Fake response error", + }, }, - }, - ]; - - await expect(client.timestampToEvent(roomId, 0, Direction.Forward)).rejects.toBeDefined(); - - expect(mocked(client.http.authedRequest).mock.calls.length).toStrictEqual(1); - const [method, path, queryParams, , { prefix } = { prefix: undefined }] = mocked(client.http.authedRequest) - .mock.calls[0]; - expect(method).toStrictEqual("GET"); - expect(prefix).toStrictEqual(ClientPrefix.V1); - expect(path).toStrictEqual(`/rooms/${encodeURIComponent(roomId)}/timestamp_to_event`); - expect(queryParams).toStrictEqual({ - ts: "0", - dir: "f", - }); + ], + true, + ); }); }); From 628bcbf33a3dfa3d685ee5bf4bc933beca30590f Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 6 Jan 2023 15:22:58 +0000 Subject: [PATCH 62/88] Fall back to the unstable endpoint if we receive a 405 status --- spec/unit/matrix-client.spec.ts | 18 +++++++++++++++++- src/client.ts | 5 ++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 9a2ae6c978a..4756544f301 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -417,7 +417,7 @@ describe("MatrixClient", function () { ]); }); - it("should fallback to unstable endpoint when no support for stable endpoint", async () => { + it("should fallback to unstable endpoint when stable endpoint 404s", async () => { await assertRequestsMade([ { prefix: ClientPrefix.V1, @@ -433,6 +433,22 @@ describe("MatrixClient", function () { ]); }); + 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", async () => { await assertRequestsMade( [ diff --git a/src/client.ts b/src/client.ts index e36ef7c410b..7ec95929927 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9391,7 +9391,10 @@ export class MatrixClient extends TypedEventEmittererr).httpStatus === 400 || // This the correct standard status code for an unsupported // endpoint according to MSC3743. - (err).httpStatus === 404) + (err).httpStatus === 404 || + // This the correct standard status code for an invalid + // method according to MSC3743. + (err).httpStatus === 405) ) { return await this.http.authedRequest(Method.Get, path, queryParams, undefined, { prefix: "/_matrix/client/unstable/org.matrix.msc3030", From 12cc7be31cd7f7891e1f89bdd1636d536e596e75 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 6 Jan 2023 15:33:48 +0000 Subject: [PATCH 63/88] Test 400, 429 and 502 responses --- spec/unit/matrix-client.spec.ts | 48 ++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 4756544f301..ba1c94a2986 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -417,6 +417,22 @@ describe("MatrixClient", function () { ]); }); + 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([ { @@ -449,7 +465,7 @@ describe("MatrixClient", function () { ]); }); - it("should not fallback to unstable endpoint when stable endpoint returns an error", async () => { + it("should not fallback to unstable endpoint when stable endpoint returns an error (500)", async () => { await assertRequestsMade( [ { @@ -463,6 +479,36 @@ describe("MatrixClient", function () { 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()", () => { From 981acf0044f89a325c637eb24ed2e6fa53e8faab Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 6 Jan 2023 16:38:02 +0000 Subject: [PATCH 64/88] Rename test to fit renamed function. Co-authored-by: Eric Eastwood --- spec/unit/matrix-client.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 8e31e3bfa14..8d73939a54a 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -100,7 +100,7 @@ type WrappedRoom = Room & { _state: Map; }; -describe("convertQueryDictToStringRecord", () => { +describe("convertQueryDictToMap", () => { it("returns an empty map when dict is undefined", () => { expect(convertQueryDictToMap(undefined)).toEqual(new Map()); }); From fdb80ad2594bea4905a1c8945eb66fb383254b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 6 Jan 2023 18:09:01 +0100 Subject: [PATCH 65/88] Remove video track when muting video (#3028) --- spec/unit/webrtc/groupCall.spec.ts | 8 +++++-- spec/unit/webrtc/mediaHandler.spec.ts | 18 +++++++------- src/webrtc/call.ts | 31 +++++++++++++++++++++--- src/webrtc/groupCall.ts | 3 +++ src/webrtc/mediaHandler.ts | 34 ++++++++++++++++----------- 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/spec/unit/webrtc/groupCall.spec.ts b/spec/unit/webrtc/groupCall.spec.ts index 281497bbf62..7d822ec334e 100644 --- a/spec/unit/webrtc/groupCall.spec.ts +++ b/spec/unit/webrtc/groupCall.spec.ts @@ -810,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)[] = []; @@ -824,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()); 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/webrtc/call.ts b/src/webrtc/call.ts index d6fde54ed94..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 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; From c3d422f5fb1efac400da4c4ade592db3831445f9 Mon Sep 17 00:00:00 2001 From: Clark Fischer <439978+clarkf@users.noreply.github.com> Date: Fri, 6 Jan 2023 22:37:05 +0000 Subject: [PATCH 66/88] Remove 'qs' dependency (#3033) 'qs' appears to be unused since 34c5598 (PR #2719). Signed-off-by: Clark Fischer Signed-off-by: Clark Fischer --- package.json | 1 - yarn.lock | 7 ------- 2 files changed, 8 deletions(-) diff --git a/package.json b/package.json index 5e6d6574c09..a152039ea10 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "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": "9" diff --git a/yarn.lock b/yarn.lock index 0f09a00c784..03a855db843 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5872,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" From 8a4c95ee72eaa6de87731fad96e7deb268ac14a9 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 9 Jan 2023 11:54:19 +0000 Subject: [PATCH 67/88] Tests for getVisibleRooms --- spec/unit/matrix-client.spec.ts | 79 +++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 58e6a579990..bd788dbc3b9 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -60,6 +60,7 @@ 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(); @@ -1967,4 +1968,82 @@ describe("MatrixClient", function () { 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); + }); + }); }); From c7c16256dfd0ead12f30f21a64544b8dbf8d66ec Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 9 Jan 2023 16:53:52 +0000 Subject: [PATCH 68/88] Update comment to reflect commonality between 404 and 405 status --- src/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index f39bbf718ef..6a7735b51bf 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9423,10 +9423,10 @@ export class MatrixClient extends TypedEventEmittererr).httpStatus === 400 || // This the correct standard status code for an unsupported - // endpoint according to MSC3743. + // endpoint according to MSC3743. Not Found and Method Not Allowed + // both indicate that this endpoint+verb combination is + // not supported. (err).httpStatus === 404 || - // This the correct standard status code for an invalid - // method according to MSC3743. (err).httpStatus === 405) ) { return await this.http.authedRequest(Method.Get, path, queryParams, undefined, { From 7de41644445df4b3582c5e05f54ca4397970c089 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 9 Jan 2023 13:46:35 +0000 Subject: [PATCH 69/88] Factor out a (public) function to find a room's predecessor --- spec/unit/room.spec.ts | 49 ++++++++++++++++++++++++++++++++++++++++++ src/client.ts | 10 +++------ src/models/room.ts | 18 ++++++++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 81f6602e784..9a50fdc9559 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, @@ -3225,4 +3226,52 @@ describe("Room", function () { expect(room.getBlacklistUnverifiedDevices()).toBe(false); }); }); + + describe("findPredecessorRoomId", () => { + 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: "spec_is_not_clear_what_id_this_is", + 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", null as unknown as MatrixClient, "@u:example.com"); + expect(room.findPredecessorRoomId()).toBeNull(); + }); + + it("Returns null if the create event has no predecessor", () => { + const room = new Room("roomid", null as unknown as MatrixClient, "@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", null as unknown as MatrixClient, "@u:example.com"); + room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]); + expect(room.findPredecessorRoomId()).toBe("replacedroomid"); + }); + }); }); diff --git a/src/client.ts b/src/client.ts index 6a7735b51bf..551119307e2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3770,13 +3770,9 @@ export class MatrixClient extends TypedEventEmitter { 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 createEvent = this.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); From 999e355136ebb06d6a2ade516fe4d573b6b91ea5 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 9 Jan 2023 16:41:43 +0000 Subject: [PATCH 70/88] Use Room.getLiveTimeline instead of deprecated this.currentState --- src/models/room.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/models/room.ts b/src/models/room.ts index cf8c2948105..68cb461dcd5 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -2974,7 +2974,12 @@ export class Room extends ReadReceipt { * this room has no predecessor. */ public findPredecessorRoomId(): string | null { - const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, ""); + 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) { From c6090325b386c3ba31e49722e1971ce25b99fae8 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 10 Jan 2023 13:29:06 +0000 Subject: [PATCH 71/88] Update id string to reflect spec --- spec/unit/room.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 9a50fdc9559..5f6f355a315 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -3242,7 +3242,7 @@ describe("Room", function () { }; if (predecessorRoomId) { content.predecessor = { - event_id: "spec_is_not_clear_what_id_this_is", + event_id: "id_of_last_known_event", room_id: predecessorRoomId, }; } From 185ded4ebc259d35f6c4c4945a68dba793703519 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 10 Jan 2023 09:19:55 -0700 Subject: [PATCH 72/88] Remove extensible events v1 field population on legacy events (#3040) * Remove extensible events v1 field population on legacy events With extensible events v2, affected events are now gated by a room version, so we don't need this code anymore. The proposal has generally moved away from mixing m.room.message with extensible fields as well. * Run prettier * Remove unstable identifier from tests too * Run prettier again --- spec/integ/matrix-client-methods.spec.ts | 18 +++++----- src/client.ts | 42 ++---------------------- 2 files changed, 11 insertions(+), 49 deletions(-) diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index c1c4c30dc5d..01e24ded4ea 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -1177,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" }); @@ -1197,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/src/client.ts b/src/client.ts index 551119307e2..c7c75b35dbb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -18,7 +18,7 @@ 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 type { IMegolmSessionData } from "./@types/crypto"; import { ISyncStateData, SyncApi, SyncApiOptions, SyncState } from "./sync"; @@ -4541,44 +4541,8 @@ 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); } From 8e29f8ead0bd0341563469ab43a6ac87ae1c39e8 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 11 Jan 2023 09:53:27 +0000 Subject: [PATCH 73/88] Improve hasUserReadEvent and getUserReadUpTo realibility with threads (#3031) Co-authored-by: Patrick Cloke Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- spec/unit/models/thread.spec.ts | 186 ++++++++++++++++++++++++++++++-- spec/unit/notifications.spec.ts | 30 +++++- spec/unit/room.spec.ts | 15 ++- src/models/room.ts | 40 ++++++- src/models/thread.ts | 85 +++++++++++++-- 5 files changed, 332 insertions(+), 24 deletions(-) diff --git a/spec/unit/models/thread.spec.ts b/spec/unit/models/thread.spec.ts index 42b976cd6ac..99f090a53b7 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 { 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,148 @@ 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); }); }); }); 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/room.spec.ts b/spec/unit/room.spec.ts index 5f6f355a315..38fc2cdc42a 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -50,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"; @@ -3228,6 +3229,14 @@ describe("Room", function () { }); 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; @@ -3258,18 +3267,18 @@ describe("Room", function () { } it("Returns null if there is no create event", () => { - const room = new Room("roomid", null as unknown as MatrixClient, "@u:example.com"); + 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", null as unknown as MatrixClient, "@u:example.com"); + 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", null as unknown as MatrixClient, "@u:example.com"); + const room = new Room("roomid", client!, "@u:example.com"); room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]); expect(room.findPredecessorRoomId()).toBe("replacedroomid"); }); diff --git a/src/models/room.ts b/src/models/room.ts index 68cb461dcd5..a363ef0dfa3 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. @@ -309,6 +309,13 @@ export class Room extends ReadReceipt { private notificationCounts: NotificationCount = {}; private readonly threadNotifications = 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 @@ -2727,6 +2734,17 @@ export class Room extends ReadReceipt { { 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); + } }); }); }); @@ -3300,6 +3318,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 25e454c32e2..2ffee8038c2 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,7 +22,7 @@ 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"; @@ -504,17 +504,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; } } From 3e48c76a7780e578382b56e04525a005e5ee0e3f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Jan 2023 12:02:56 +0000 Subject: [PATCH 74/88] Avoid use of mkdirp (#3050) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a152039ea10..8841c0b284b 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", "build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly", "build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src", - "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.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", From 3ce582d0049b6790f3c3104f3f008035f0bedfc3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Jan 2023 13:08:30 +0000 Subject: [PATCH 75/88] Fix dated example (#3049) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79d6417c409..9099f8ec44d 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,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)); }); From 3e97067b3ead2a34a2b25fb8db594ed35955d3de Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 11 Jan 2023 13:29:02 +0000 Subject: [PATCH 76/88] Prepare changelog for v23.1.0-rc.1 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e05be659ebf..a977edc232f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +Changes in [23.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.1) (2023-01-11) +============================================================================================================ + +## 🦖 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. + Changes in [23.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.0.0) (2022-12-21) ================================================================================================== From 1d87f5b163bbb431318f4eb49fb442f4d9ec1070 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 11 Jan 2023 13:29:05 +0000 Subject: [PATCH 77/88] v23.1.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8841c0b284b..040c83bacc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "23.0.0", + "version": "23.1.0-rc.1", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=16.0.0" @@ -32,7 +32,7 @@ "keywords": [ "matrix-org" ], - "main": "./src/index.ts", + "main": "./lib/index.js", "browser": "./lib/browser-index.ts", "matrix_src_main": "./src/index.ts", "matrix_src_browser": "./src/browser-index.ts", @@ -142,5 +142,6 @@ "outputDirectory": "coverage", "outputName": "jest-sonar-report.xml", "relativePaths": true - } + }, + "typings": "./lib/index.d.ts" } From f35298a326f4d60c0f9fec2c7848177bef6d1c98 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Jan 2023 16:31:02 +0000 Subject: [PATCH 78/88] [Release] Fix browser entrypoint (#3052) --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 040c83bacc5..ae4a3a2a05b 100644 --- a/package.json +++ b/package.json @@ -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", From 6b7efbcd91aa918145136532046e7196dbd59afc Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Thu, 12 Jan 2023 13:31:18 +0000 Subject: [PATCH 79/88] Prepare changelog for v23.1.0-rc.2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a977edc232f..56cbb6e5bf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [23.1.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.2) (2023-01-12) +============================================================================================================ + +## 🐛 Bug Fixes + * Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013. + Changes in [23.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.1) (2023-01-11) ============================================================================================================ From c10152e098d19695f14b5e7dde93c936d3409a3e Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Thu, 12 Jan 2023 13:31:20 +0000 Subject: [PATCH 80/88] v23.1.0-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ae4a3a2a05b..8e26a120a57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "23.1.0-rc.1", + "version": "23.1.0-rc.2", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=16.0.0" From 39cf1863f19ab19edad05eeff8e3a3ad8d5307a1 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Thu, 12 Jan 2023 17:10:42 +0000 Subject: [PATCH 81/88] Fix failure to start in firefox private browser (#3058) (#3059) (cherry picked from commit aa1e118f18c2264e5cfa634f31dda0a039b64f12) Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- src/client.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index c7c75b35dbb..91cc21be0b0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1690,12 +1690,16 @@ export class MatrixClient extends TypedEventEmitter { - logger.error(`Failed to remove IndexedDB instance ${dbname}: ${e}`); - reject(new Error(`Error clearing storage: ${e}`)); + // 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}`); - //reject(new Error(`Error clearing storage: ${e}`)); }; }); await prom; From bc2a182ee988afdb6327e48bffcad7d413c83e45 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 13 Jan 2023 10:40:29 +0000 Subject: [PATCH 82/88] Prepare changelog for v23.1.0-rc.3 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56cbb6e5bf1..d60734dc565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [23.1.0-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.3) (2023-01-13) +============================================================================================================ + +## 🐛 Bug Fixes + * 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. + Changes in [23.1.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.2) (2023-01-12) ============================================================================================================ From 94f1eda830a8807e11be5d533a4d3eb3c3863765 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 13 Jan 2023 10:40:31 +0000 Subject: [PATCH 83/88] v23.1.0-rc.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e26a120a57..f81f82bc419 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "23.1.0-rc.2", + "version": "23.1.0-rc.3", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=16.0.0" From 3e693fab2388d88a08bd84a503bc6df93acc0a6f Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Tue, 17 Jan 2023 09:06:28 +0000 Subject: [PATCH 84/88] [Backport staging] Correctly handle limited sync responses by resetting the thread timeline (#3069) Co-authored-by: Janne Mareike Koschinski --- spec/unit/models/thread.spec.ts | 141 +++++++++++++++++++++++++++++++- src/models/room.ts | 5 +- src/models/thread.ts | 59 ++++++++++++- 3 files changed, 202 insertions(+), 3 deletions(-) diff --git a/spec/unit/models/thread.spec.ts b/spec/unit/models/thread.spec.ts index 99f090a53b7..4dd5a681b6e 100644 --- a/spec/unit/models/thread.spec.ts +++ b/spec/unit/models/thread.spec.ts @@ -20,7 +20,7 @@ import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/t import { mkThread } from "../../test-utils/thread"; import { TestClient } from "../../TestClient"; import { emitPromise, mkMessage, mock } from "../../test-utils/test-utils"; -import { EventStatus, MatrixEvent } from "../../../src"; +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"; @@ -283,4 +283,143 @@ describe("Thread", () => { 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/src/models/room.ts b/src/models/room.ts index a363ef0dfa3..e1202c523d1 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1114,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(); } @@ -1223,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); } diff --git a/src/models/thread.ts b/src/models/thread.ts index 2ffee8038c2..31587bbafb7 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -256,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) { @@ -358,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(); From 97df6db49c5d03147eeae3bbeea4908421804df4 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 17 Jan 2023 09:11:52 +0000 Subject: [PATCH 85/88] Prepare changelog for v23.1.0-rc.4 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d60734dc565..37d618c1e7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [23.1.0-rc.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.4) (2023-01-17) +============================================================================================================ + +## 🐛 Bug Fixes + * 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.1.0-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.3) (2023-01-13) ============================================================================================================ From 4179f2978d8955a1dfc2beb38a22e3087d29d812 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 17 Jan 2023 09:11:54 +0000 Subject: [PATCH 86/88] v23.1.0-rc.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f81f82bc419..4d61bb094b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "23.1.0-rc.3", + "version": "23.1.0-rc.4", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=16.0.0" From 81f3aef960f06c55c4aa4aba2d6215f59c43e681 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 18 Jan 2023 13:30:19 +0000 Subject: [PATCH 87/88] Prepare changelog for v23.1.0 --- CHANGELOG.md | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37d618c1e7e..0ab66068d20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,5 @@ -Changes in [23.1.0-rc.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.4) (2023-01-17) -============================================================================================================ - -## 🐛 Bug Fixes - * 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.1.0-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.3) (2023-01-13) -============================================================================================================ - -## 🐛 Bug Fixes - * 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. - -Changes in [23.1.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.2) (2023-01-12) -============================================================================================================ - -## 🐛 Bug Fixes - * Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013. - -Changes in [23.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.1) (2023-01-11) -============================================================================================================ +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)). @@ -39,10 +21,13 @@ Changes in [23.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta * 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)). + * 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) ================================================================================================== From b8711f15fdec5817d5d98ea1dcc889246bc306d2 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 18 Jan 2023 13:30:21 +0000 Subject: [PATCH 88/88] v23.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d61bb094b7..0c5c9851618 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "23.1.0-rc.4", + "version": "23.1.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=16.0.0"