From ddce1bcd28c4fd0d756694fc2f4e6e77af2e4600 Mon Sep 17 00:00:00 2001 From: Clark Fischer Date: Mon, 16 Jan 2023 07:35:23 -0800 Subject: [PATCH 01/45] Add async `setImmediate` util Adds an async/promise-based version of `setImmediate`. Note that, despite being poorly adopted, `setImmediate` is polyfilled, and should be more performant than `sleep(0)`. Signed-off-by: Clark Fischer --- spec/unit/utils.spec.ts | 34 ++++++++++++++++++++++++++++++++++ src/utils.ts | 14 +++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/spec/unit/utils.spec.ts b/spec/unit/utils.spec.ts index 8104cba0815..caf15a43e6d 100644 --- a/spec/unit/utils.spec.ts +++ b/spec/unit/utils.spec.ts @@ -1,3 +1,19 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import * as utils from "../../src/utils"; import { alphabetPad, @@ -587,4 +603,22 @@ describe("utils", function () { expect(utils.isSupportedReceiptType("this is a receipt type")).toBeFalsy(); }); }); + + describe("sleep", () => { + it("resolves", async () => { + await utils.sleep(0); + }); + + it("resolves with the provided value", async () => { + const expected = Symbol("hi"); + const result = await utils.sleep(0, expected); + expect(result).toBe(expected); + }); + }); + + describe("immediate", () => { + it("resolves", async () => { + await utils.immediate(); + }); + }); }); diff --git a/src/utils.ts b/src/utils.ts index 5134c8a4d06..6a15e97444c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016, 2019, 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. @@ -392,13 +391,22 @@ export function ensureNoTrailingSlash(url?: string): string | undefined { } } -// Returns a promise which resolves with a given value after the given number of ms +/** + * Returns a promise which resolves with a given value after the given number of ms + */ export function sleep(ms: number, value?: T): Promise { return new Promise((resolve) => { setTimeout(resolve, ms, value); }); } +/** + * Promise/async version of {@link setImmediate}. + */ +export function immediate(): Promise { + return new Promise(setImmediate); +} + export function isNullOrUndefined(val: any): boolean { return val === null || val === undefined; } From b76e7ca7826ba3073a42e539204a88f6104d372e Mon Sep 17 00:00:00 2001 From: Clark Fischer Date: Sun, 8 Jan 2023 10:43:49 -0800 Subject: [PATCH 02/45] Reduce blocking while pre-fetching Megolm keys Currently, calling `Client#prepareToEncrypt` in a megolm room has the potential to block for multiple seconds while it crunches numbers. Sleeping for 0 seconds (approximating `setImmediate`) allows the engine to process other events, updates, or re-renders in between checks. See - https://github.com/vector-im/element-web/issues/21612 - https://github.com/vector-im/element-web/issues/11836 Signed-off-by: Clark Fischer --- spec/unit/crypto/algorithms/megolm.spec.ts | 84 ++++++++++++++++++++-- src/crypto/algorithms/megolm.ts | 9 ++- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index 973ec0bd24c..0008501af10 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.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. @@ -16,6 +16,7 @@ limitations under the License. import { mocked, MockedObject } from "jest-mock"; +import type { DeviceInfoMap } from "../../../../src/crypto/DeviceList"; import "../../../olm-loader"; import type { OutboundGroupSession } from "@matrix-org/olm"; import * as algorithms from "../../../../src/crypto/algorithms"; @@ -33,6 +34,7 @@ import { ClientEvent, MatrixClient, RoomMember } from "../../../../src"; import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo"; import { DeviceTrustLevel } from "../../../../src/crypto/CrossSigning"; import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm"; +import { sleep } from "../../../../src/utils"; const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!; const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!; @@ -58,6 +60,12 @@ describe("MegolmDecryption", function () { beforeEach(async function () { mockCrypto = testUtils.mock(Crypto, "Crypto") as MockedObject; + + // @ts-ignore assigning to readonly prop + mockCrypto.backupManager = { + backupGroupSession: () => {}, + }; + mockBaseApis = { claimOneTimeKeys: jest.fn(), sendToDevice: jest.fn(), @@ -314,10 +322,6 @@ describe("MegolmDecryption", function () { let olmDevice: OlmDevice; beforeEach(async () => { - // @ts-ignore assigning to readonly prop - mockCrypto.backupManager = { - backupGroupSession: () => {}, - }; const cryptoStore = new MemoryCryptoStore(); olmDevice = new OlmDevice(cryptoStore); @@ -515,6 +519,76 @@ describe("MegolmDecryption", function () { }); }); + describe("prepareToEncrypt", () => { + let megolm: MegolmEncryptionClass; + let room: jest.Mocked; + + const deviceMap: DeviceInfoMap = { + "user-a": { + "device-a": new DeviceInfo("device-a"), + "device-b": new DeviceInfo("device-b"), + "device-c": new DeviceInfo("device-c"), + }, + "user-b": { + "device-d": new DeviceInfo("device-d"), + "device-e": new DeviceInfo("device-e"), + "device-f": new DeviceInfo("device-f"), + }, + "user-c": { + "device-g": new DeviceInfo("device-g"), + "device-h": new DeviceInfo("device-h"), + "device-i": new DeviceInfo("device-i"), + }, + }; + + beforeEach(() => { + room = testUtils.mock(Room, "Room") as jest.Mocked; + room.getEncryptionTargetMembers.mockImplementation(async () => [ + new RoomMember(room.roomId, "@user:example.org"), + ]); + room.getBlacklistUnverifiedDevices.mockReturnValue(false); + + mockCrypto.downloadKeys.mockImplementation(async () => deviceMap); + + mockCrypto.checkDeviceTrust.mockImplementation(() => new DeviceTrustLevel(true, true, true, true)); + + const olmDevice = new OlmDevice(new MemoryCryptoStore()); + megolm = new MegolmEncryptionClass({ + userId: "@user:id", + deviceId: "12345", + crypto: mockCrypto, + olmDevice, + baseApis: mockBaseApis, + roomId: room.roomId, + config: { + algorithm: "m.megolm.v1.aes-sha2", + rotation_period_ms: 9_999_999, + }, + }); + }); + + it("checks each device", async () => { + megolm.prepareToEncrypt(room); + //@ts-ignore private member access, gross + await megolm.encryptionPreparation?.promise; + + for (const userId in deviceMap) { + for (const deviceId in deviceMap[userId]) { + expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledWith(userId, deviceId); + } + } + }); + + it("defers before completing", async () => { + megolm.prepareToEncrypt(room); + // Ensure that `Crypto#checkDeviceTrust` has been called *fewer* + // than the full nine times, after yielding once. + await sleep(0); + const callCount = mockCrypto.checkDeviceTrust.mock.calls.length; + expect(callCount).toBeLessThan(9); + }); + }); + it("notifies devices that have been blocked", async function () { const aliceClient = new TestClient("@alice:example.com", "alicedevice").client; const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client; diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index 163d3953d45..e7daf4b7cec 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -1,5 +1,5 @@ /* -Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2015 - 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. @@ -43,6 +43,7 @@ import { IMegolmEncryptedContent, IncomingRoomKeyRequest, IEncryptedContent } fr import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager"; import { OlmGroupSessionExtraData } from "../../@types/crypto"; import { MatrixError } from "../../http-api"; +import { immediate } from "../../utils"; // determine whether the key can be shared with invitees export function isRoomSharedHistory(room: Room): boolean { @@ -73,7 +74,6 @@ export interface IOlmDevice { deviceInfo: T; } -/* eslint-disable camelcase */ export interface IOutboundGroupSessionKey { chain_index: number; key: string; @@ -106,7 +106,6 @@ interface IPayload extends Partial { algorithm?: string; sender_key?: string; } -/* eslint-enable camelcase */ interface SharedWithData { // The identity key of the device we shared with @@ -1213,6 +1212,10 @@ export class MegolmEncryption extends EncryptionAlgorithm { continue; } + // Yield prior to checking each device so that we don't block + // updating/rendering for too long. + // See https://github.com/vector-im/element-web/issues/21612 + await immediate(); const deviceTrust = this.crypto.checkDeviceTrust(userId, deviceId); if ( From 1ee487a2ff48b33d6250fc1828f4d2e02f0fa534 Mon Sep 17 00:00:00 2001 From: Clark Fischer Date: Sun, 8 Jan 2023 13:55:03 -0800 Subject: [PATCH 03/45] Make prepareToEncrypt cancellable. NOTE: This commit introduces a backwards-compatible API change. Adds the ability to cancel `MegolmEncryption#prepareToEncrypt` by returning a cancellation function. The bulk of the processing happens in `getDevicesInRoom`, which now accepts a 'getter' that allows the caller to indicate cancellation. See https://github.com/matrix-org/matrix-js-sdk/issues/1255 Closes #1255 Signed-off-by: Clark Fischer --- spec/unit/crypto/algorithms/megolm.spec.ts | 11 +++++ src/crypto/algorithms/megolm.ts | 57 ++++++++++++++++++---- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index 0008501af10..9f0ffae9976 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -587,6 +587,17 @@ describe("MegolmDecryption", function () { const callCount = mockCrypto.checkDeviceTrust.mock.calls.length; expect(callCount).toBeLessThan(9); }); + + it("is cancellable", async () => { + const stop = megolm.prepareToEncrypt(room); + + const before = mockCrypto.checkDeviceTrust.mock.calls.length; + stop(); + + // Ensure that no more devices were checked after cancellation. + await sleep(10); + expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledTimes(before); + }); }); it("notifies devices that have been blocked", async function () { diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index e7daf4b7cec..ff7e29264c8 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -222,6 +222,7 @@ export class MegolmEncryption extends EncryptionAlgorithm { private encryptionPreparation?: { promise: Promise; startTime: number; + cancel: () => void; }; protected readonly roomId: string; @@ -973,30 +974,36 @@ export class MegolmEncryption extends EncryptionAlgorithm { * send, in order to speed up sending of the message. * * @param room - the room the event is in + * @returns A function that, when called, will stop the preparation */ - public prepareToEncrypt(room: Room): void { + public prepareToEncrypt(room: Room): () => void { if (room.roomId !== this.roomId) { throw new Error("MegolmEncryption.prepareToEncrypt called on unexpected room"); } if (this.encryptionPreparation != null) { // We're already preparing something, so don't do anything else. - // FIXME: check if we need to restart - // (https://github.com/matrix-org/matrix-js-sdk/issues/1255) const elapsedTime = Date.now() - this.encryptionPreparation.startTime; this.prefixedLogger.debug( `Already started preparing to encrypt for this room ${elapsedTime}ms ago, skipping`, ); - return; + return this.encryptionPreparation.cancel; } this.prefixedLogger.debug("Preparing to encrypt events"); + let cancelled = false; + const isCancelled = (): boolean => cancelled; + this.encryptionPreparation = { startTime: Date.now(), promise: (async (): Promise => { try { - const [devicesInRoom, blocked] = await this.getDevicesInRoom(room); + // Attempt to enumerate the devices in room, and gracefully + // handle cancellation if it occurs. + const getDevicesResult = await this.getDevicesInRoom(room, false, isCancelled); + if (getDevicesResult === null) return; + const [devicesInRoom, blocked] = getDevicesResult; if (this.crypto.globalErrorOnUnknownDevices) { // Drop unknown devices for now. When the message gets sent, we'll @@ -1015,7 +1022,16 @@ export class MegolmEncryption extends EncryptionAlgorithm { delete this.encryptionPreparation; } })(), + + cancel: (): void => { + // The caller has indicated that the process should be cancelled, + // so tell the promise that we'd like to halt, and reset the preparation state. + cancelled = true; + delete this.encryptionPreparation; + }, }; + + return this.encryptionPreparation.cancel; } /** @@ -1164,17 +1180,32 @@ export class MegolmEncryption extends EncryptionAlgorithm { * * @param forceDistributeToUnverified - if set to true will include the unverified devices * even if setting is set to block them (useful for verification) + * @param isCancelled - will cause the procedure to abort early if and when it starts + * returning `true`. If omitted, cancellation won't happen. * - * @returns Promise which resolves to an array whose - * first element is a map from userId to deviceId to deviceInfo indicating + * @returns Promise which resolves to `null`, or an array whose + * first element is a {@link DeviceInfoMap} indicating * the devices that messages should be encrypted to, and whose second * element is a map from userId to deviceId to data indicating the devices - * that are in the room but that have been blocked + * that are in the room but that have been blocked. + * If `isCancelled` is provided and returns `true` while processing, `null` + * will be returned. + * If `isCancelled` is not provided, the Promise will never resolve to `null`. */ + private async getDevicesInRoom( + room: Room, + forceDistributeToUnverified?: boolean, + ): Promise<[DeviceInfoMap, IBlockedMap]>; + private async getDevicesInRoom( + room: Room, + forceDistributeToUnverified?: boolean, + isCancelled?: () => boolean, + ): Promise; private async getDevicesInRoom( room: Room, forceDistributeToUnverified = false, - ): Promise<[DeviceInfoMap, IBlockedMap]> { + isCancelled?: () => boolean, + ): Promise { const members = await room.getEncryptionTargetMembers(); this.prefixedLogger.debug( `Encrypting for users (shouldEncryptForInvitedMembers: ${room.shouldEncryptForInvitedMembers()}):`, @@ -1200,6 +1231,11 @@ export class MegolmEncryption extends EncryptionAlgorithm { // See https://github.com/vector-im/element-web/issues/2305 for details. const devices = await this.crypto.downloadKeys(roomMembers, false); const blocked: IBlockedMap = {}; + + if (isCancelled?.() === true) { + return null; + } + // remove any blocked devices for (const userId in devices) { if (!devices.hasOwnProperty(userId)) { @@ -1215,7 +1251,8 @@ export class MegolmEncryption extends EncryptionAlgorithm { // Yield prior to checking each device so that we don't block // updating/rendering for too long. // See https://github.com/vector-im/element-web/issues/21612 - await immediate(); + if (isCancelled !== undefined) await immediate(); + if (isCancelled?.() === true) return null; const deviceTrust = this.crypto.checkDeviceTrust(userId, deviceId); if ( From 7ed787b86a09c01679affffddcc25729184520fd Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 24 Jan 2023 13:44:03 +0000 Subject: [PATCH 04/45] Fix bug in getRoomUpgradeHistory's verifyLinks functionality (#3089) --- spec/unit/matrix-client.spec.ts | 140 +++++++++++++++++++++++++++----- src/client.ts | 4 +- 2 files changed, 122 insertions(+), 22 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index edfd6d7e9c4..163a77b5b35 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -2205,7 +2205,7 @@ describe("MatrixClient", function () { "creator": "@daryl:alexandria.example.com", "m.federate": true, "predecessor": { - event_id: "spec_is_not_clear_what_id_this_is", + event_id: "id_of_last_event", room_id: predecessorRoomId, }, "room_version": "9", @@ -2278,34 +2278,34 @@ describe("MatrixClient", function () { }); describe("getRoomUpgradeHistory", () => { - function createRoomHistory(): [Room, Room, Room, Room] { + /** + * Create a chain of room history with create events and tombstones. + * + * @param creates include create events (default=true) + * @param tombstones include tomstone events (default=true) + * @returns 4 rooms chained together with tombstones and create + * events, in order from oldest to latest. + */ + function createRoomHistory(creates = true, tombstones = true): [Room, Room, Room, Room] { const room1 = new Room("room1", client, "@carol:alexandria.example.com"); const room2 = new Room("room2", client, "@daryl:alexandria.example.com"); const room3 = new Room("room3", client, "@rick:helicopter.example.com"); const room4 = new Room("room4", client, "@michonne:hawthorne.example.com"); - room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], {}); - room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)]); - - room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], {}); - room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)]); + if (creates) { + room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)]); + room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)]); + room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)]); + } - room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], {}); - room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)]); + if (tombstones) { + room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], {}); + room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], {}); + room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], {}); + } mocked(store.getRoom).mockImplementation((roomId: string) => { - switch (roomId) { - case "room1": - return room1; - case "room2": - return room2; - case "room3": - return room3; - case "room4": - return room4; - default: - return null; - } + return { room1, room2, room3, room4 }[roomId] || null; }); return [room1, room2, room3, room4]; @@ -2334,6 +2334,49 @@ describe("MatrixClient", function () { ]); }); + it("Returns the predecessors of this room (with verify links)", () => { + const [room1, room2, room3, room4] = createRoomHistory(); + const verifyLinks = true; + const history = client.getRoomUpgradeHistory(room4.roomId, verifyLinks); + expect(history.map((room) => room.roomId)).toEqual([ + room1.roomId, + room2.roomId, + room3.roomId, + room4.roomId, + ]); + }); + + it("With verify links, rejects predecessors that don't point forwards", () => { + // Given successors point back with create events, but + // predecessors do not point forwards with tombstones + const [, , , room4] = createRoomHistory(true, false); + + // When I ask for history with verifyLinks on + const verifyLinks = true; + const history = client.getRoomUpgradeHistory(room4.roomId, verifyLinks); + + // Then the predecessors are not included in the history + expect(history.map((room) => room.roomId)).toEqual([room4.roomId]); + }); + + it("Without verify links, includes predecessors that don't point forwards", () => { + // Given successors point back with create events, but + // predecessors do not point forwards with tombstones + const [room1, room2, room3, room4] = createRoomHistory(true, false); + + // When I ask for history with verifyLinks off + const verifyLinks = false; + const history = client.getRoomUpgradeHistory(room4.roomId, verifyLinks); + + // Then the predecessors are included in the history + expect(history.map((room) => room.roomId)).toEqual([ + room1.roomId, + room2.roomId, + room3.roomId, + room4.roomId, + ]); + }); + it("Returns the subsequent rooms", () => { const [room1, room2, room3, room4] = createRoomHistory(); const history = client.getRoomUpgradeHistory(room1.roomId); @@ -2345,6 +2388,49 @@ describe("MatrixClient", function () { ]); }); + it("Returns the subsequent rooms (with verify links)", () => { + const [room1, room2, room3, room4] = createRoomHistory(); + const verifyLinks = true; + const history = client.getRoomUpgradeHistory(room1.roomId, verifyLinks); + expect(history.map((room) => room.roomId)).toEqual([ + room1.roomId, + room2.roomId, + room3.roomId, + room4.roomId, + ]); + }); + + it("With verify links, rejects successors that don't point backwards", () => { + // Given predecessors point forwards with tombstones, but + // successors do not point back with create events. + const [room1, , ,] = createRoomHistory(false, true); + + // When I ask for history with verifyLinks on + const verifyLinks = true; + const history = client.getRoomUpgradeHistory(room1.roomId, verifyLinks); + + // Then the successors are not included in the history + expect(history.map((room) => room.roomId)).toEqual([room1.roomId]); + }); + + it("Without verify links, includes predecessors that don't point forwards", () => { + // Given predecessors point forwards with tombstones, but + // successors do not point back with create events. + const [room1, room2, room3, room4] = createRoomHistory(false, true); + + // When I ask for history with verifyLinks off + const verifyLinks = false; + const history = client.getRoomUpgradeHistory(room1.roomId, verifyLinks); + + // Then the successors are included in the history + expect(history.map((room) => room.roomId)).toEqual([ + room1.roomId, + room2.roomId, + room3.roomId, + room4.roomId, + ]); + }); + it("Returns the predecessors and subsequent rooms", () => { const [room1, room2, room3, room4] = createRoomHistory(); const history = client.getRoomUpgradeHistory(room3.roomId); @@ -2355,6 +2441,18 @@ describe("MatrixClient", function () { room4.roomId, ]); }); + + it("Returns the predecessors and subsequent rooms (with verify links)", () => { + const [room1, room2, room3, room4] = createRoomHistory(); + const verifyLinks = true; + const history = client.getRoomUpgradeHistory(room3.roomId, verifyLinks); + expect(history.map((room) => room.roomId)).toEqual([ + room1.roomId, + room2.roomId, + room3.roomId, + room4.roomId, + ]); + }); }); }); }); diff --git a/src/client.ts b/src/client.ts index 2a579777117..86432d8c0da 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4996,6 +4996,7 @@ export class MatrixClient extends TypedEventEmitter Date: Tue, 24 Jan 2023 18:19:19 +0000 Subject: [PATCH 05/45] Remove video tracks on video mute without renegotiating --- src/webrtc/call.ts | 71 +++++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 8f88e34ba04..f6e91f789d6 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -387,6 +387,10 @@ export class MatrixCall extends TypedEventEmitter; + /** * Construct a new Matrix Call. * @param opts - Config options. @@ -480,7 +484,9 @@ export class MatrixCall extends TypedEventEmitter feed.purpose === SDPStreamMetadataPurpose.Usermedia); } @@ -1292,18 +1306,7 @@ export class MatrixCall extends TypedEventEmitter t.kind === kind)) { - this.peerConn?.removeTrack(sender); - } - } - // Thirdly, we replace the old tracks, if possible. + // Then replace the old tracks, if possible. for (const track of stream.getTracks()) { const tKey = getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, track.kind); @@ -1361,22 +1364,51 @@ export class MatrixCall extends TypedEventEmitter { logger.log(`call ${this.callId} setLocalVideoMuted ${muted}`); + + // if we were still thinking about stopping and removing the video + // track: don't, because we want it back. + if (!muted && this.stopVideoTrackTimer !== undefined) { + clearTimeout(this.stopVideoTrackTimer); + this.stopVideoTrackTimer = undefined; + } + if (!(await this.client.getMediaHandler().hasVideoDevice())) { return this.isLocalVideoMuted(); } - if (!this.hasLocalUserMediaVideoTrack && !muted) { + if (!this.hasUserMediaVideoSender && !muted) { await this.upgradeCall(false, true); return this.isLocalVideoMuted(); } - if (this.opponentSupportsSDPStreamMetadata()) { - const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted); + + // we may not have a video track - if not, re-request usermedia + if (!muted && this.localUsermediaStream!.getVideoTracks().length === 0) { + const stream = await this.client.getMediaHandler().getUserMediaStream(true, true); await this.updateLocalUsermediaStream(stream); - } else { - this.localUsermediaFeed?.setAudioVideoMuted(null, muted); } + + this.localUsermediaFeed?.setAudioVideoMuted(null, muted); + this.updateMuteStatus(); await this.sendMetadataUpdate(); + + // if we're muting video, set a timeout to stop & remove the video track so we release + // the camera. We wait a short time to do this because when we disable a track, WebRTC + // will send black video for it. If we just stop and remove it straight away, the video + // will just freeze which means that when we unmute video, the other side will briefly + // get a static frame of us from before we muted. This way, the still frame is just black. + // A very small delay is not always enough so the theory here is that it needs to be long + // enough for WebRTC to encode a frame: 120ms should be long enough even if we're only + // doing 10fps. + if (muted) { + this.stopVideoTrackTimer = setTimeout(() => { + for (const t of this.localUsermediaStream!.getVideoTracks()) { + t.stop(); + this.localUsermediaStream!.removeTrack(t); + } + }, 120); + } + return this.isLocalVideoMuted(); } @@ -1404,7 +1436,7 @@ export class MatrixCall extends TypedEventEmitter Date: Tue, 24 Jan 2023 20:30:10 +0000 Subject: [PATCH 06/45] Remove timer on call terminate --- src/webrtc/call.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index f6e91f789d6..ff656777473 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -2498,6 +2498,8 @@ export class MatrixCall extends TypedEventEmitter { if (this.callHasEnded()) return; + if (this.stopVideoTrackTimer !== undefined) clearTimeout(this.stopVideoTrackTimer); + this.hangupParty = hangupParty; this.hangupReason = hangupReason; this.state = CallState.Ended; From 66ae985af592cdc5e9a9f92bb8a91cc8a4069327 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 25 Jan 2023 08:48:56 +0000 Subject: [PATCH 07/45] Refactor getRoomUpgradeHistory to use Room.findPredecessorRoomId (#3090) * Refactor getRoomUpgradeHistory to use Room.findPredecessorRoomId * Simplify getRoomUpgradeHistory implementation a little --- src/client.ts | 86 +++++++++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/src/client.ts b/src/client.ts index 86432d8c0da..ae2afe9ca93 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4990,68 +4990,72 @@ export class MatrixClient extends TypedEventEmitter ref.roomId)); - if (roomIds.size < upgradeHistory.length) { + ret.push(successorRoom); + const roomIds = new Set(ret.map((ref) => ref.roomId)); + if (roomIds.size < ret.length) { // The last room added to the list introduced a previous roomId // To avoid recursion, return the last rooms - 1 - return upgradeHistory.slice(0, upgradeHistory.length - 1); + return ret.slice(0, ret.length - 1); } // Set the current room to the reference room so we know where we're at - currentRoom = refRoom; - tombstoneEvent = currentRoom.currentState.getStateEvents(EventType.RoomTombstone, ""); + room = successorRoom; + tombstoneEvent = room.currentState.getStateEvents(EventType.RoomTombstone, ""); } - - return upgradeHistory; + return ret; } /** From ce2a9d70362cd31d9fd2f8f88a5928ece5298a71 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 25 Jan 2023 10:59:03 +0000 Subject: [PATCH 08/45] Fix test --- spec/unit/webrtc/call.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index ce966fbb79f..838809691e9 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -40,6 +40,7 @@ import { MockMediaStreamTrack, installWebRTCMocks, MockRTCPeerConnection, + MockRTCRtpTransceiver, SCREENSHARE_STREAM_ID, } from "../../test-utils/webrtc"; import { CallFeed } from "../../../src/webrtc/callFeed"; @@ -536,6 +537,13 @@ describe("Call", function () { it("if local video", async () => { call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" }); + // since this is testing for the presence of a local sender, we need to add a transciever + // rather than just a source track + (call as any).transceivers.set( + "m.usermedia:video", + new MockRTCRtpTransceiver(call.peerConn as unknown as MockRTCPeerConnection), + ); + (call as any).pushNewLocalFeed( new MockMediaStream("remote_stream1", [new MockMediaStreamTrack("track_id", "video")]), SDPStreamMetadataPurpose.Usermedia, From b328b72cd587c6151cc9ae6e82a28be0197d585f Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 25 Jan 2023 11:07:14 +0000 Subject: [PATCH 09/45] Move timeout clear to be with its friends --- src/webrtc/call.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index ff656777473..f601c90f61e 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -2498,8 +2498,6 @@ export class MatrixCall extends TypedEventEmitter { if (this.callHasEnded()) return; - if (this.stopVideoTrackTimer !== undefined) clearTimeout(this.stopVideoTrackTimer); - this.hangupParty = hangupParty; this.hangupReason = hangupReason; this.state = CallState.Ended; @@ -2512,6 +2510,10 @@ export class MatrixCall extends TypedEventEmitter Date: Wed, 25 Jan 2023 11:16:19 +0000 Subject: [PATCH 10/45] Actually check we have a sender, not just a transceiver --- spec/unit/webrtc/call.spec.ts | 11 ++++++----- src/webrtc/call.ts | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index 838809691e9..8bedb34f809 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -42,6 +42,7 @@ import { MockRTCPeerConnection, MockRTCRtpTransceiver, SCREENSHARE_STREAM_ID, + MockRTCRtpSender, } from "../../test-utils/webrtc"; import { CallFeed } from "../../../src/webrtc/callFeed"; import { EventType, IContent, ISendEventResponse, MatrixEvent, Room } from "../../../src"; @@ -539,13 +540,13 @@ describe("Call", function () { // since this is testing for the presence of a local sender, we need to add a transciever // rather than just a source track - (call as any).transceivers.set( - "m.usermedia:video", - new MockRTCRtpTransceiver(call.peerConn as unknown as MockRTCPeerConnection), - ); + const mockTrack = new MockMediaStreamTrack("track_id", "video"); + const mockTransceiver = new MockRTCRtpTransceiver(call.peerConn as unknown as MockRTCPeerConnection); + mockTransceiver.sender = new MockRTCRtpSender(mockTrack) as unknown as RTCRtpSender; + (call as any).transceivers.set("m.usermedia:video", mockTransceiver); (call as any).pushNewLocalFeed( - new MockMediaStream("remote_stream1", [new MockMediaStreamTrack("track_id", "video")]), + new MockMediaStream("remote_stream1", [mockTrack]), SDPStreamMetadataPurpose.Usermedia, false, ); diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index f601c90f61e..98c8e027bf4 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -514,7 +514,7 @@ export class MatrixCall extends TypedEventEmitter Date: Wed, 25 Jan 2023 11:20:25 +0000 Subject: [PATCH 11/45] Check we have both a sender and a track to send --- src/webrtc/call.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 98c8e027bf4..3665b9d18a4 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -1436,7 +1436,7 @@ export class MatrixCall extends TypedEventEmitter Date: Wed, 25 Jan 2023 14:27:02 +0000 Subject: [PATCH 12/45] Remove flaky test (#3098) I introduced a flaky test to confirm that `MegolmEncryption#prepareToEncrypt` didn't block the main thread too much, but it turns out that, when run in varying environments, it tends to fail. The same behavior is guaranteed by the following cancellation test - if the thread is blocked, it can't be cancelled. Signed-off-by: Clark Fischer Signed-off-by: Clark Fischer --- spec/unit/crypto/algorithms/megolm.spec.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index 9f0ffae9976..6c91d530f28 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -579,15 +579,6 @@ describe("MegolmDecryption", function () { } }); - it("defers before completing", async () => { - megolm.prepareToEncrypt(room); - // Ensure that `Crypto#checkDeviceTrust` has been called *fewer* - // than the full nine times, after yielding once. - await sleep(0); - const callCount = mockCrypto.checkDeviceTrust.mock.calls.length; - expect(callCount).toBeLessThan(9); - }); - it("is cancellable", async () => { const stop = megolm.prepareToEncrypt(room); From b09b33eb4c95108e5e4bd094f9bb4c3339bb17f2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 25 Jan 2023 15:06:36 +0000 Subject: [PATCH 13/45] Add tests --- spec/unit/webrtc/call.spec.ts | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index 8bedb34f809..d782c61a903 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -838,6 +838,55 @@ describe("Call", function () { await startVideoCall(client, call); }); + afterEach(() => { + jest.useRealTimers(); + }); + + it("should not remove video sender on video mute", async () => { + await call.setLocalVideoMuted(true); + expect((call as any).hasUserMediaVideoSender).toBe(true); + }); + + it("should release camera after short delay on video mute", async () => { + jest.useFakeTimers(); + + await call.setLocalVideoMuted(true); + + jest.advanceTimersByTime(500); + + expect(call.hasLocalUserMediaVideoTrack).toBe(false); + }); + + it("should re-request video feed on video unmute if it doesn't have one", async () => { + jest.useFakeTimers(); + + const mockGetUserMediaStream = jest + .fn() + .mockReturnValue(client.client.getMediaHandler().getUserMediaStream(true, true)); + + client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream; + + await call.setLocalVideoMuted(true); + + jest.advanceTimersByTime(500); + + await call.setLocalVideoMuted(false); + + expect(mockGetUserMediaStream).toHaveBeenCalled(); + }); + + it("should not release camera on fast mute and unmute", async () => { + const mockGetUserMediaStream = jest.fn(); + + client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream; + + await call.setLocalVideoMuted(true); + await call.setLocalVideoMuted(false); + + expect(mockGetUserMediaStream).not.toHaveBeenCalled(); + expect(call.hasLocalUserMediaVideoTrack).toBe(true); + }); + describe("sending sdp_stream_metadata_changed events", () => { it("should send sdp_stream_metadata_changed when muting audio", async () => { await call.setMicrophoneMuted(true); From a18d4e226ebdd0dbfea26bf77da7e7ba50eea0c4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 25 Jan 2023 15:07:51 +0000 Subject: [PATCH 14/45] Actually check audio sender too --- src/webrtc/call.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 3665b9d18a4..e0c8310a338 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -510,7 +510,7 @@ export class MatrixCall extends TypedEventEmitter Date: Thu, 26 Jan 2023 15:07:55 +1300 Subject: [PATCH 15/45] Poll model (#3036) * first cut poll model * process incoming poll relations * allow alt event types in relations model * allow alt event types in relations model * remove unneccesary checks on remove relation * comment * Revert "allow alt event types in relations model" This reverts commit e578d84464403d4a15ee8a7cf3ac643f4fb86d69. * Revert "Revert "allow alt event types in relations model"" This reverts commit 515db7a8bc2df5a1c619a37c86e17ccbe287ba7a. * basic handling for new poll relations * tests * test room.processPollEvents * join processBeaconEvents and poll events in client * tidy and set 23 copyrights * use rooms instance of matrixClient * tidy * more copyright * simplify processPollEvent code * throw when poll start event has no roomId * updates for events-sdk move * more type changes for events-sdk changes * comment --- spec/unit/models/poll.spec.ts | 246 ++++++++++++++++++++++++++++++++++ spec/unit/room.spec.ts | 76 ++++++++++- src/client.ts | 28 ++-- src/matrix.ts | 1 + src/models/poll.ts | 175 ++++++++++++++++++++++++ src/models/relations.ts | 4 +- src/models/room.ts | 44 +++++- 7 files changed, 561 insertions(+), 13 deletions(-) create mode 100644 spec/unit/models/poll.spec.ts create mode 100644 src/models/poll.ts diff --git a/spec/unit/models/poll.spec.ts b/spec/unit/models/poll.spec.ts new file mode 100644 index 00000000000..feb0c27ffab --- /dev/null +++ b/spec/unit/models/poll.spec.ts @@ -0,0 +1,246 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { IEvent, MatrixEvent, PollEvent } from "../../../src"; +import { REFERENCE_RELATION } from "../../../src/@types/extensible_events"; +import { M_POLL_END, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE } from "../../../src/@types/polls"; +import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent"; +import { Poll } from "../../../src/models/poll"; +import { getMockClientWithEventEmitter } from "../../test-utils/client"; + +jest.useFakeTimers(); + +describe("Poll", () => { + const mockClient = getMockClientWithEventEmitter({ + relations: jest.fn(), + }); + const roomId = "!room:server"; + // 14.03.2022 16:15 + const now = 1647270879403; + + const basePollStartEvent = new MatrixEvent({ + ...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(), + room_id: roomId, + }); + basePollStartEvent.event.event_id = "$12345"; + + beforeEach(() => { + jest.clearAllMocks(); + jest.setSystemTime(now); + + mockClient.relations.mockResolvedValue({ events: [] }); + }); + + let eventId = 1; + const makeRelatedEvent = (eventProps: Partial, timestamp = now): MatrixEvent => { + const event = new MatrixEvent({ + ...eventProps, + content: { + ...(eventProps.content || {}), + "m.relates_to": { + rel_type: REFERENCE_RELATION.name, + event_id: basePollStartEvent.getId(), + }, + }, + }); + event.event.origin_server_ts = timestamp; + event.event.event_id = `${eventId++}`; + return event; + }; + + it("initialises with root event", () => { + const poll = new Poll(basePollStartEvent, mockClient); + expect(poll.roomId).toEqual(roomId); + expect(poll.pollId).toEqual(basePollStartEvent.getId()); + expect(poll.pollEvent).toEqual(basePollStartEvent.unstableExtensibleEvent); + expect(poll.isEnded).toBe(false); + }); + + it("throws when poll start has no room id", () => { + const pollStartEvent = new MatrixEvent( + PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(), + ); + expect(() => new Poll(pollStartEvent, mockClient)).toThrowError("Invalid poll start event."); + }); + + it("throws when poll start has no event id", () => { + const pollStartEvent = new MatrixEvent({ + ...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(), + room_id: roomId, + }); + expect(() => new Poll(pollStartEvent, mockClient)).toThrowError("Invalid poll start event."); + }); + + describe("fetching responses", () => { + it("calls relations api and emits", async () => { + const poll = new Poll(basePollStartEvent, mockClient); + const emitSpy = jest.spyOn(poll, "emit"); + const responses = await poll.getResponses(); + expect(mockClient.relations).toHaveBeenCalledWith(roomId, basePollStartEvent.getId(), "m.reference"); + expect(emitSpy).toHaveBeenCalledWith(PollEvent.Responses, responses); + }); + + it("returns existing responses object after initial fetch", async () => { + const poll = new Poll(basePollStartEvent, mockClient); + const responses = await poll.getResponses(); + const responses2 = await poll.getResponses(); + // only fetched relations once + expect(mockClient.relations).toHaveBeenCalledTimes(1); + // strictly equal + expect(responses).toBe(responses2); + }); + + it("waits for existing relations request to finish when getting responses", async () => { + const poll = new Poll(basePollStartEvent, mockClient); + const firstResponsePromise = poll.getResponses(); + const secondResponsePromise = poll.getResponses(); + await firstResponsePromise; + expect(firstResponsePromise).toEqual(secondResponsePromise); + await secondResponsePromise; + expect(mockClient.relations).toHaveBeenCalledTimes(1); + }); + + it("filters relations for relevent response events", async () => { + const replyEvent = new MatrixEvent({ type: "m.room.message" }); + const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! }); + const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable }); + + mockClient.relations.mockResolvedValue({ + events: [replyEvent, stableResponseEvent, unstableResponseEvent], + }); + const poll = new Poll(basePollStartEvent, mockClient); + const responses = await poll.getResponses(); + expect(responses.getRelations()).toEqual([stableResponseEvent, unstableResponseEvent]); + }); + + describe("with poll end event", () => { + const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable! }); + const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable! }); + const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000); + const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now); + const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000); + + beforeEach(() => { + mockClient.relations.mockResolvedValue({ + events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, stablePollEndEvent], + }); + }); + + it("sets poll end event with stable event type", async () => { + const poll = new Poll(basePollStartEvent, mockClient); + jest.spyOn(poll, "emit"); + await poll.getResponses(); + + expect(poll.isEnded).toBe(true); + expect(poll.emit).toHaveBeenCalledWith(PollEvent.End); + }); + + it("sets poll end event with unstable event type", async () => { + mockClient.relations.mockResolvedValue({ + events: [unstablePollEndEvent], + }); + const poll = new Poll(basePollStartEvent, mockClient); + jest.spyOn(poll, "emit"); + await poll.getResponses(); + + expect(poll.isEnded).toBe(true); + expect(poll.emit).toHaveBeenCalledWith(PollEvent.End); + }); + + it("filters out responses that were sent after poll end", async () => { + const poll = new Poll(basePollStartEvent, mockClient); + const responses = await poll.getResponses(); + + // just response type events + // and response with ts after poll end event is excluded + expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]); + }); + }); + }); + + describe("onNewRelation()", () => { + it("discards response if poll responses have not been initialised", () => { + const poll = new Poll(basePollStartEvent, mockClient); + jest.spyOn(poll, "emit"); + const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now); + + poll.onNewRelation(responseEvent); + + // did not add response -> no emit + expect(poll.emit).not.toHaveBeenCalled(); + }); + + it("sets poll end event when responses are not initialised", () => { + const poll = new Poll(basePollStartEvent, mockClient); + jest.spyOn(poll, "emit"); + const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable! }); + + poll.onNewRelation(stablePollEndEvent); + + expect(poll.emit).toHaveBeenCalledWith(PollEvent.End); + }); + + it("sets poll end event and refilters responses based on timestamp", async () => { + const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable! }); + const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000); + const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now); + const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000); + mockClient.relations.mockResolvedValue({ + events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd], + }); + const poll = new Poll(basePollStartEvent, mockClient); + const responses = await poll.getResponses(); + jest.spyOn(poll, "emit"); + + expect(responses.getRelations().length).toEqual(3); + poll.onNewRelation(stablePollEndEvent); + + expect(poll.emit).toHaveBeenCalledWith(PollEvent.End); + expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses); + expect(responses.getRelations().length).toEqual(2); + // after end timestamp event is removed + expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]); + }); + + it("filters out irrelevant relations", async () => { + const poll = new Poll(basePollStartEvent, mockClient); + // init responses + const responses = await poll.getResponses(); + jest.spyOn(poll, "emit"); + const replyEvent = new MatrixEvent({ type: "m.room.message" }); + + poll.onNewRelation(replyEvent); + + // did not add response -> no emit + expect(poll.emit).not.toHaveBeenCalled(); + expect(responses.getRelations().length).toEqual(0); + }); + + it("adds poll response relations to responses", async () => { + const poll = new Poll(basePollStartEvent, mockClient); + // init responses + const responses = await poll.getResponses(); + jest.spyOn(poll, "emit"); + const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now); + + poll.onNewRelation(responseEvent); + + // did not add response -> no emit + expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses); + expect(responses.getRelations()).toEqual([responseEvent]); + }); + }); +}); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 38fc2cdc42a..705199ddbdd 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.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,6 +19,7 @@ limitations under the License. */ import { mocked } from "jest-mock"; +import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, PollStartEvent } from "matrix-events-sdk"; import * as utils from "../test-utils/test-utils"; import { emitPromise } from "../test-utils/test-utils"; @@ -37,6 +38,7 @@ import { MatrixEvent, MatrixEventEvent, PendingEventOrdering, + PollEvent, RelationType, RoomEvent, RoomMember, @@ -3228,6 +3230,78 @@ describe("Room", function () { }); }); + describe("processPollEvents()", () => { + let room: Room; + let client: MatrixClient; + + beforeEach(() => { + client = getMockClientWithEventEmitter({ + decryptEventIfNeeded: jest.fn(), + }); + room = new Room(roomId, client, userA); + jest.spyOn(room, "emit").mockClear(); + }); + + const makePollStart = (id: string): MatrixEvent => { + const event = new MatrixEvent({ + ...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(), + room_id: roomId, + }); + event.event.event_id = id; + return event; + }; + + it("adds poll models to room state for a poll start event ", async () => { + const pollStartEvent = makePollStart("1"); + const events = [pollStartEvent]; + + await room.processPollEvents(events); + expect(client.decryptEventIfNeeded).toHaveBeenCalledWith(pollStartEvent); + const pollInstance = room.polls.get(pollStartEvent.getId()!); + expect(pollInstance).toBeTruthy(); + + expect(room.emit).toHaveBeenCalledWith(PollEvent.New, pollInstance); + }); + + it("adds related events to poll models", async () => { + const pollStartEvent = makePollStart("1"); + const pollStartEvent2 = makePollStart("2"); + const events = [pollStartEvent, pollStartEvent2]; + const pollResponseEvent = new MatrixEvent({ + type: M_POLL_RESPONSE.name, + content: { + "m.relates_to": { + rel_type: RelationType.Reference, + event_id: pollStartEvent.getId(), + }, + }, + }); + const messageEvent = new MatrixEvent({ + type: "m.room.messsage", + content: { + text: "hello", + }, + }); + + // init poll + await room.processPollEvents(events); + + const poll = room.polls.get(pollStartEvent.getId()!)!; + const poll2 = room.polls.get(pollStartEvent2.getId()!)!; + jest.spyOn(poll, "onNewRelation"); + jest.spyOn(poll2, "onNewRelation"); + + await room.processPollEvents([pollResponseEvent, messageEvent]); + + // only called for relevant event + expect(poll.onNewRelation).toHaveBeenCalledTimes(1); + expect(poll.onNewRelation).toHaveBeenCalledWith(pollResponseEvent); + + // only called on poll with relation + expect(poll2.onNewRelation).not.toHaveBeenCalled(); + }); + }); + describe("findPredecessorRoomId", () => { let client: MatrixClient | null = null; beforeEach(() => { diff --git a/src/client.ts b/src/client.ts index ae2afe9ca93..869737e7425 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5426,7 +5426,7 @@ export class MatrixClient extends TypedEventEmitter it.getServerAggregatedRelation(THREAD_RELATION_TYPE.name)), @@ -9360,10 +9360,22 @@ export class MatrixClient extends TypedEventEmitter void; + [PollEvent.Destroy]: (pollIdentifier: string) => void; + [PollEvent.End]: () => void; + [PollEvent.Responses]: (responses: Relations) => void; +}; + +const filterResponseRelations = ( + relationEvents: MatrixEvent[], + pollEndTimestamp: number, +): { + responseEvents: MatrixEvent[]; +} => { + const responseEvents = relationEvents.filter((event) => { + if (event.isDecryptionFailure()) { + // @TODO(kerrya) PSG-1023 track and return these + return; + } + return ( + M_POLL_RESPONSE.matches(event.getType()) && + // From MSC3381: + // "Votes sent on or before the end event's timestamp are valid votes" + event.getTs() <= pollEndTimestamp + ); + }); + + return { responseEvents }; +}; + +export class Poll extends TypedEventEmitter, PollEventHandlerMap> { + public readonly roomId: string; + public readonly pollEvent: PollStartEvent; + private fetchingResponsesPromise: null | Promise = null; + private responses: null | Relations = null; + private endEvent: MatrixEvent | undefined; + + public constructor(private rootEvent: MatrixEvent, private matrixClient: MatrixClient) { + super(); + if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) { + throw new Error("Invalid poll start event."); + } + this.roomId = this.rootEvent.getRoomId()!; + // @TODO(kerrya) proper way to do this? + this.pollEvent = this.rootEvent.unstableExtensibleEvent as unknown as PollStartEvent; + } + + public get pollId(): string { + return this.rootEvent.getId()!; + } + + public get isEnded(): boolean { + return !!this.endEvent; + } + + public async getResponses(): Promise { + // if we have already fetched the responses + // just return them + if (this.responses) { + return this.responses; + } + if (!this.fetchingResponsesPromise) { + this.fetchingResponsesPromise = this.fetchResponses(); + } + await this.fetchingResponsesPromise; + return this.responses!; + } + + /** + * + * @param event - event with a relation to the rootEvent + * @returns void + */ + public onNewRelation(event: MatrixEvent): void { + if (M_POLL_END.matches(event.getType())) { + this.endEvent = event; + this.refilterResponsesOnEnd(); + this.emit(PollEvent.End); + } + + // wait for poll responses to be initialised + if (!this.responses) { + return; + } + + const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER; + const { responseEvents } = filterResponseRelations([event], pollEndTimestamp); + + if (responseEvents.length) { + responseEvents.forEach((event) => { + this.responses!.addEvent(event); + }); + this.emit(PollEvent.Responses, this.responses); + } + } + + private async fetchResponses(): Promise { + // we want: + // - stable and unstable M_POLL_RESPONSE + // - stable and unstable M_POLL_END + // so make one api call and filter by event type client side + const allRelations = await this.matrixClient.relations(this.roomId, this.rootEvent.getId()!, "m.reference"); + + // @TODO(kerrya) paging results + + const responses = new Relations("m.reference", M_POLL_RESPONSE.name, this.matrixClient, [ + M_POLL_RESPONSE.altName!, + ]); + + const pollEndEvent = allRelations.events.find((event) => M_POLL_END.matches(event.getType())); + const pollCloseTimestamp = pollEndEvent?.getTs() || Number.MAX_SAFE_INTEGER; + + const { responseEvents } = filterResponseRelations(allRelations.events, pollCloseTimestamp); + + responseEvents.forEach((event) => { + responses.addEvent(event); + }); + + this.responses = responses; + this.endEvent = pollEndEvent; + if (this.endEvent) { + this.emit(PollEvent.End); + } + this.emit(PollEvent.Responses, this.responses); + } + + /** + * Only responses made before the poll ended are valid + * Refilter after an end event is recieved + * To ensure responses are valid + */ + private refilterResponsesOnEnd(): void { + if (!this.responses) { + return; + } + + const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER; + this.responses.getRelations().forEach((event) => { + if (event.getTs() > pollEndTimestamp) { + this.responses?.removeEvent(event); + } + }); + + this.emit(PollEvent.Responses, this.responses); + } +} diff --git a/src/models/relations.ts b/src/models/relations.ts index 069bb0a0c69..d2b637cc3c6 100644 --- a/src/models/relations.ts +++ b/src/models/relations.ts @@ -1,5 +1,5 @@ /* -Copyright 2019, 2021 The Matrix.org Foundation C.I.C. +Copyright 2019, 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. @@ -122,7 +122,7 @@ export class Relations extends TypedEventEmitter { + public async removeEvent(event: MatrixEvent): Promise { if (!this.relations.has(event)) { return; } diff --git a/src/models/room.ts b/src/models/room.ts index e1202c523d1..003dc59df6e 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Optional } from "matrix-events-sdk"; +import { M_POLL_START, Optional } from "matrix-events-sdk"; import { EventTimelineSet, @@ -65,6 +65,7 @@ import { IStateEventWithRoomId } from "../@types/search"; import { RelationsContainer } from "./relations-container"; import { ReadReceipt, synthesizeReceipt } from "./read-receipt"; import { Feature, ServerSupport } from "../feature"; +import { Poll, PollEvent } from "./poll"; // These constants are used as sane defaults when the homeserver doesn't support // the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be @@ -162,7 +163,8 @@ export type RoomEmittedEvents = | BeaconEvent.New | BeaconEvent.Update | BeaconEvent.Destroy - | BeaconEvent.LivenessChange; + | BeaconEvent.LivenessChange + | PollEvent.New; export type RoomEventHandlerMap = { /** @@ -289,6 +291,11 @@ export type RoomEventHandlerMap = { [RoomEvent.UnreadNotifications]: (unreadNotifications?: NotificationCount, threadId?: string) => void; [RoomEvent.TimelineRefresh]: (room: Room, eventTimelineSet: EventTimelineSet) => void; [ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void; + /** + * Fires when a new poll instance is added to the room state + * @param poll - the new poll + */ + [PollEvent.New]: (poll: Poll) => void; } & Pick & EventTimelineSetHandlerMap & Pick & @@ -317,6 +324,7 @@ export class Room extends ReadReceipt { */ private unthreadedReceipts = new Map(); private readonly timelineSets: EventTimelineSet[]; + public readonly polls: Map = new Map(); public readonly threadsTimelineSets: EventTimelineSet[] = []; // any filtered timeline sets we're maintaining for this room private readonly filteredTimelineSets: Record = {}; // filter_id: timelineSet @@ -1890,6 +1898,38 @@ export class Room extends ReadReceipt { this.threadsReady = true; } + public async processPollEvents(events: MatrixEvent[]): Promise { + const processPollStartEvent = (event: MatrixEvent): void => { + if (!M_POLL_START.matches(event.getType())) return; + try { + const poll = new Poll(event, this.client); + this.polls.set(event.getId()!, poll); + this.emit(PollEvent.New, poll); + } catch {} + // poll creation can fail for malformed poll start events + }; + + const processPollRelationEvent = (event: MatrixEvent): void => { + const relationEventId = event.relationEventId; + if (relationEventId && this.polls.has(relationEventId)) { + const poll = this.polls.get(relationEventId); + poll?.onNewRelation(event); + } + }; + + const processPollEvent = (event: MatrixEvent): void => { + processPollStartEvent(event); + processPollRelationEvent(event); + }; + + for (const event of events) { + try { + await this.client.decryptEventIfNeeded(event); + processPollEvent(event); + } catch {} + } + } + /** * Fetch a single page of threadlist messages for the specific thread filter * @internal From fee5a006f19983c68cf68d5a173634b8489d4a96 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jan 2023 09:41:21 +0000 Subject: [PATCH 16/45] chore(deps): update dependency rimraf to v4 (#3103) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4796be39956..b47f92f79aa 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "jest-mock": "^29.0.0", "matrix-mock-request": "^2.5.0", "prettier": "2.8.2", - "rimraf": "^3.0.2", + "rimraf": "^4.0.0", "terser": "^5.5.1", "tsify": "^5.0.2", "typedoc": "^0.23.20", diff --git a/yarn.lock b/yarn.lock index a9776dac67d..c2376374388 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6189,6 +6189,11 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.1.2.tgz#20dfbc98083bdfaa28b01183162885ef213dbf7c" + integrity sha512-BlIbgFryTbw3Dz6hyoWFhKk+unCcHMSkZGrTFVAx2WmttdBSonsdtRlwiuTbDqTKr+UlXIUqJVS4QT5tUzGENQ== + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" From 22f5e41058dae2bbf561d159323fcef4f5c0b745 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jan 2023 09:41:33 +0000 Subject: [PATCH 17/45] chore(deps): update dependency @types/uuid to v9 (#3102) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b47f92f79aa..15ef53ad239 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "@types/jest": "^29.0.0", "@types/node": "18", "@types/sdp-transform": "^2.4.5", - "@types/uuid": "7", + "@types/uuid": "9", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "allchange": "^1.0.6", diff --git a/yarn.lock b/yarn.lock index c2376374388..dbe4f503de6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1755,10 +1755,10 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== -"@types/uuid@7": - version "7.0.5" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-7.0.5.tgz#b1d2f772142a301538fae9bdf9cf15b9f2573a29" - integrity sha512-hKB88y3YHL8oPOs/CNlaXtjWn93+Bs48sDQR37ZUqG2tLeCS7EA1cmnkKsuQsub9OKEB/y/Rw9zqJqqNSbqVlQ== +"@types/uuid@9": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.0.tgz#53ef263e5239728b56096b0a869595135b7952d2" + integrity sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q== "@types/webidl-conversions@*": version "7.0.0" From f446b49e49e408b9b3f88f49843664155a587ef9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jan 2023 09:41:40 +0000 Subject: [PATCH 18/45] fix(deps): update dependency @babel/runtime to v7.20.13 (#3100) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index dbe4f503de6..11bb8dfbf01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -989,9 +989,9 @@ source-map-support "^0.5.16" "@babel/runtime@^7.12.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" - integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== + version "7.20.13" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" + integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== dependencies: regenerator-runtime "^0.13.11" From cb2fab64d8f2743293c24f60b5fbc6cc4f44f062 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jan 2023 09:41:59 +0000 Subject: [PATCH 19/45] chore(deps): update typescript-eslint monorepo to v5.48.2 (#3097) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 92 +++++++++++++++++++++++++++---------------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/yarn.lock b/yarn.lock index 11bb8dfbf01..35c3b04adde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1778,13 +1778,13 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.45.0": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.1.tgz#deee67e399f2cb6b4608c935777110e509d8018c" - integrity sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ== + version "5.49.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.49.0.tgz#d0b4556f0792194bf0c2fb297897efa321492389" + integrity sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q== dependencies: - "@typescript-eslint/scope-manager" "5.48.1" - "@typescript-eslint/type-utils" "5.48.1" - "@typescript-eslint/utils" "5.48.1" + "@typescript-eslint/scope-manager" "5.49.0" + "@typescript-eslint/type-utils" "5.49.0" + "@typescript-eslint/utils" "5.49.0" debug "^4.3.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" @@ -1793,71 +1793,71 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.45.0": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.1.tgz#d0125792dab7e232035434ab8ef0658154db2f10" - integrity sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA== + version "5.49.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.49.0.tgz#d699734b2f20e16351e117417d34a2bc9d7c4b90" + integrity sha512-veDlZN9mUhGqU31Qiv2qEp+XrJj5fgZpJ8PW30sHU+j/8/e5ruAhLaVDAeznS7A7i4ucb/s8IozpDtt9NqCkZg== dependencies: - "@typescript-eslint/scope-manager" "5.48.1" - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/typescript-estree" "5.48.1" + "@typescript-eslint/scope-manager" "5.49.0" + "@typescript-eslint/types" "5.49.0" + "@typescript-eslint/typescript-estree" "5.49.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.48.1.tgz#39c71e4de639f5fe08b988005beaaf6d79f9d64d" - integrity sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ== +"@typescript-eslint/scope-manager@5.49.0": + version "5.49.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.49.0.tgz#81b5d899cdae446c26ddf18bd47a2f5484a8af3e" + integrity sha512-clpROBOiMIzpbWNxCe1xDK14uPZh35u4QaZO1GddilEzoCLAEz4szb51rBpdgurs5k2YzPtJeTEN3qVbG+LRUQ== dependencies: - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/visitor-keys" "5.48.1" + "@typescript-eslint/types" "5.49.0" + "@typescript-eslint/visitor-keys" "5.49.0" -"@typescript-eslint/type-utils@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.48.1.tgz#5d94ac0c269a81a91ad77c03407cea2caf481412" - integrity sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ== +"@typescript-eslint/type-utils@5.49.0": + version "5.49.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.49.0.tgz#8d5dcc8d422881e2ccf4ebdc6b1d4cc61aa64125" + integrity sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA== dependencies: - "@typescript-eslint/typescript-estree" "5.48.1" - "@typescript-eslint/utils" "5.48.1" + "@typescript-eslint/typescript-estree" "5.49.0" + "@typescript-eslint/utils" "5.49.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.48.1.tgz#efd1913a9aaf67caf8a6e6779fd53e14e8587e14" - integrity sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg== +"@typescript-eslint/types@5.49.0": + version "5.49.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.49.0.tgz#ad66766cb36ca1c89fcb6ac8b87ec2e6dac435c3" + integrity sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg== -"@typescript-eslint/typescript-estree@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.1.tgz#9efa8ee2aa471c6ab62e649f6e64d8d121bc2056" - integrity sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA== +"@typescript-eslint/typescript-estree@5.49.0": + version "5.49.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.49.0.tgz#ebd6294c0ea97891fce6af536048181e23d729c8" + integrity sha512-PBdx+V7deZT/3GjNYPVQv1Nc0U46dAHbIuOG8AZ3on3vuEKiPDwFE/lG1snN2eUB9IhF7EyF7K1hmTcLztNIsA== dependencies: - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/visitor-keys" "5.48.1" + "@typescript-eslint/types" "5.49.0" + "@typescript-eslint/visitor-keys" "5.49.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.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.48.1.tgz#20f2f4e88e9e2a0961cbebcb47a1f0f7da7ba7f9" - integrity sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA== +"@typescript-eslint/utils@5.49.0": + version "5.49.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.49.0.tgz#1c07923bc55ff7834dfcde487fff8d8624a87b32" + integrity sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.48.1" - "@typescript-eslint/types" "5.48.1" - "@typescript-eslint/typescript-estree" "5.48.1" + "@typescript-eslint/scope-manager" "5.49.0" + "@typescript-eslint/types" "5.49.0" + "@typescript-eslint/typescript-estree" "5.49.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.48.1": - version "5.48.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.1.tgz#79fd4fb9996023ef86849bf6f904f33eb6c8fccb" - integrity sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA== +"@typescript-eslint/visitor-keys@5.49.0": + version "5.49.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.49.0.tgz#2561c4da3f235f5c852759bf6c5faec7524f90fe" + integrity sha512-v9jBMjpNWyn8B6k/Mjt6VbUS4J1GvUlR4x3Y+ibnP1z7y7V4n0WRz+50DY6+Myj0UaXVSuUlHohO+eZ8IJEnkg== dependencies: - "@typescript-eslint/types" "5.48.1" + "@typescript-eslint/types" "5.49.0" eslint-visitor-keys "^3.3.0" JSONStream@^1.0.3: From ebd9854980ca62b7d64cf2c65a72b385d594bd82 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jan 2023 09:42:54 +0000 Subject: [PATCH 20/45] chore(deps): update dependency @types/jest to v29.2.6 (#3096) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 112 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 64 insertions(+), 48 deletions(-) diff --git a/yarn.lock b/yarn.lock index 35c3b04adde..b8cef56d0e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1188,10 +1188,10 @@ dependencies: jest-get-type "^28.0.2" -"@jest/expect-utils@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.3.1.tgz#531f737039e9b9e27c42449798acb5bba01935b6" - integrity sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g== +"@jest/expect-utils@^29.3.1", "@jest/expect-utils@^29.4.0": + version "29.4.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.4.0.tgz#97819d0da7027792888d9d2f1a41443be0baef80" + integrity sha512-w/JzTYIqjmPFIM5OOQHF9CawFx2daw1256Nzj4ZqWX96qRKbCq9WYRVqdySBKHHzuvsXLyTDIF6y61FUyrhmwg== dependencies: jest-get-type "^29.2.0" @@ -1262,12 +1262,12 @@ dependencies: "@sinclair/typebox" "^0.24.1" -"@jest/schemas@^29.0.0": - version "29.0.0" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a" - integrity sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA== +"@jest/schemas@^29.4.0": + version "29.4.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.0.tgz#0d6ad358f295cc1deca0b643e6b4c86ebd539f17" + integrity sha512-0E01f/gOZeNTG76i5eWWSupvSHaIINrTie7vCyjiYFKgzNdyEGd12BUv4oNBFHOqlHDbtoJi3HrQ38KCC90NsQ== dependencies: - "@sinclair/typebox" "^0.24.1" + "@sinclair/typebox" "^0.25.16" "@jest/source-map@^29.2.0": version "29.2.0" @@ -1331,12 +1331,12 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jest/types@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.3.1.tgz#7c5a80777cb13e703aeec6788d044150341147e3" - integrity sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA== +"@jest/types@^29.3.1", "@jest/types@^29.4.0": + version "29.4.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.0.tgz#569115f2438cacf3cff92521c7d624fbb683de3d" + integrity sha512-1S2Dt5uQp7R0bGY/L2BpuwCSji7v12kY3o8zqwlkbYBmOY956SKk+zOWqmfhHSINegiAVqOXydAYuWpzX6TYsQ== dependencies: - "@jest/schemas" "^29.0.0" + "@jest/schemas" "^29.4.0" "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" "@types/node" "*" @@ -1574,6 +1574,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== +"@sinclair/typebox@^0.25.16": + version "0.25.21" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272" + integrity sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g== + "@sinonjs/commons@^1.7.0": version "1.8.6" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" @@ -1689,9 +1694,9 @@ "@types/istanbul-lib-report" "*" "@types/jest@^29.0.0": - version "29.2.5" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.5.tgz#c27f41a9d6253f288d1910d3c5f09484a56b73c0" - integrity sha512-H2cSxkKgVmqNHXP7TC2L/WUorrZu8ZigyRywfVzv6EyBlxj39n4C00hjXYQWsbwqgElaj/CiAeSRmk5GoaKTgw== + version "29.4.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.4.0.tgz#a8444ad1704493e84dbf07bb05990b275b3b9206" + integrity sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -1771,9 +1776,9 @@ integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^17.0.8": - version "17.0.19" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.19.tgz#8dbecdc9ab48bee0cb74f6e3327de3fa0d0c98ae" - integrity sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ== + version "17.0.20" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.20.tgz#107f0fcc13bd4a524e352b41c49fe88aab5c54d5" + integrity sha512-eknWrTHofQuPk2iuqDm1waA7V6xPlbgBoaaXEgYkClhLOnB0TtbW+srJaOToAgawPxPlHQzwypFA2bhZaUGP5A== dependencies: "@types/yargs-parser" "*" @@ -3585,7 +3590,18 @@ expect@^28.1.0: jest-message-util "^28.1.3" jest-util "^28.1.3" -expect@^29.0.0, expect@^29.3.1: +expect@^29.0.0: + version "29.4.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.4.0.tgz#e2d58a73bf46399deac7db6ec16842827525ce35" + integrity sha512-pzaAwjBgLEVxBh6ZHiqb9Wv3JYuv6m8ntgtY7a48nS+2KbX0EJkPS3FQlKiTZNcqzqJHNyQsfjqN60w1hPUBfQ== + dependencies: + "@jest/expect-utils" "^29.4.0" + jest-get-type "^29.2.0" + jest-matcher-utils "^29.4.0" + jest-message-util "^29.4.0" + jest-util "^29.4.0" + +expect@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/expect/-/expect-29.3.1.tgz#92877aad3f7deefc2e3f6430dd195b92295554a6" integrity sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA== @@ -4489,15 +4505,15 @@ jest-diff@^28.1.3: jest-get-type "^28.0.2" pretty-format "^28.1.3" -jest-diff@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.3.1.tgz#d8215b72fed8f1e647aed2cae6c752a89e757527" - integrity sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw== +jest-diff@^29.3.1, jest-diff@^29.4.0: + version "29.4.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.4.0.tgz#9c75dcef5872c8889bfcb78bc9571a0e4e9bd8f6" + integrity sha512-s8KNvFx8YgdQ4fn2YLDQ7N6kmVOP68dUDVJrCHNsTc3UM5jcmyyFeYKL8EPWBQbJ0o0VvDGbWp8oYQ1nsnqnWw== dependencies: chalk "^4.0.0" diff-sequences "^29.3.1" jest-get-type "^29.2.0" - pretty-format "^29.3.1" + pretty-format "^29.4.0" jest-docblock@^29.2.0: version "29.2.0" @@ -4595,15 +4611,15 @@ jest-matcher-utils@^28.1.3: jest-get-type "^28.0.2" pretty-format "^28.1.3" -jest-matcher-utils@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.3.1.tgz#6e7f53512f80e817dfa148672bd2d5d04914a572" - integrity sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ== +jest-matcher-utils@^29.3.1, jest-matcher-utils@^29.4.0: + version "29.4.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.4.0.tgz#c2f804f95152216c8b80afbe73d82ae0ba89f652" + integrity sha512-pU4OjBn96rDdRIaPUImbPiO2ETyRVzkA1EZVu9AxBDv/XPDJ7JWfkb6IiDT5jwgicaPHMrB/fhVa6qjG6potfA== dependencies: chalk "^4.0.0" - jest-diff "^29.3.1" + jest-diff "^29.4.0" jest-get-type "^29.2.0" - pretty-format "^29.3.1" + pretty-format "^29.4.0" jest-message-util@^28.1.3: version "28.1.3" @@ -4620,18 +4636,18 @@ jest-message-util@^28.1.3: slash "^3.0.0" stack-utils "^2.0.3" -jest-message-util@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.3.1.tgz#37bc5c468dfe5120712053dd03faf0f053bd6adb" - integrity sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA== +jest-message-util@^29.3.1, jest-message-util@^29.4.0: + version "29.4.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.4.0.tgz#60d3f3dd6b5ef08ec9b698f434fbbafdb0af761d" + integrity sha512-0FvobqymmhE9pDEifvIcni9GeoKLol8eZspzH5u41g1wxYtLS60a9joT95dzzoCgrKRidNz64eaAXyzaULV8og== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.0" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.9" micromatch "^4.0.4" - pretty-format "^29.3.1" + pretty-format "^29.4.0" slash "^3.0.0" stack-utils "^2.0.3" @@ -4774,12 +4790,12 @@ jest-util@^28.1.3: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-util@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.3.1.tgz#1dda51e378bbcb7e3bc9d8ab651445591ed373e1" - integrity sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ== +jest-util@^29.3.1, jest-util@^29.4.0: + version "29.4.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.4.0.tgz#1f3743c3dda843049623501c7e6f8fa5efdc2c2f" + integrity sha512-lCCwlze7UEV8TpR9ArS8w0cTbcMry5tlBkg7QSc5og5kNyV59dnY2aKHu5fY2k5aDJMQpCUGpvL2w6ZU44lveA== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.4.0" "@types/node" "*" chalk "^4.0.0" ci-info "^3.2.0" @@ -5698,12 +5714,12 @@ pretty-format@^28.1.3: ansi-styles "^5.0.0" react-is "^18.0.0" -pretty-format@^29.0.0, pretty-format@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.3.1.tgz#1841cac822b02b4da8971dacb03e8a871b4722da" - integrity sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg== +pretty-format@^29.0.0, pretty-format@^29.3.1, pretty-format@^29.4.0: + version "29.4.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.0.tgz#766f071bb1c53f1ef8000c105bbeb649e86eb993" + integrity sha512-J+EVUPXIBHCdWAbvGBwXs0mk3ljGppoh/076g1S8qYS8nVG4u/yrhMvyTFHYYYKWnDdgRLExx0vA7pzxVGdlNw== dependencies: - "@jest/schemas" "^29.0.0" + "@jest/schemas" "^29.4.0" ansi-styles "^5.0.0" react-is "^18.0.0" From 4f9fad66e49c4b561838edce1207d8e76fd39a61 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 26 Jan 2023 10:28:07 +0000 Subject: [PATCH 21/45] MSC3946 Dynamic room predecessors (#3042) * Implement MSC3946 for getVisibleRooms * Implement MSC3946 for getRoomUpgradeHistory --- spec/unit/matrix-client.spec.ts | 217 ++++++++++++++++++++++++++++++++ spec/unit/room.spec.ts | 46 +++++++ src/@types/event.ts | 1 + src/client.ts | 33 +++-- src/models/room.ts | 14 ++- 5 files changed, 299 insertions(+), 12 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 163a77b5b35..24d3fed7727 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -2234,7 +2234,78 @@ describe("MatrixClient", function () { }); } + function predecessorEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent { + return new MatrixEvent({ + content: { + predecessor_room_id: predecessorRoomId, + }, + event_id: `predecessor_event_id_pred_${predecessorRoomId}`, + origin_server_ts: 1432735824653, + room_id: newRoomId, + sender: "@daryl:alexandria.example.com", + state_key: "", + type: "org.matrix.msc3946.room_predecessor", + }); + } + describe("getVisibleRooms", () => { + function setUpReplacedRooms(): { + room1: Room; + room2: Room; + replacedByCreate1: Room; + replacedByCreate2: Room; + replacedByDynamicPredecessor1: Room; + replacedByDynamicPredecessor2: Room; + } { + const room1 = new Room("room1", client, "@carol:alexandria.example.com"); + const replacedByCreate1 = new Room("replacedByCreate1", client, "@carol:alexandria.example.com"); + const replacedByCreate2 = new Room("replacedByCreate2", client, "@carol:alexandria.example.com"); + const replacedByDynamicPredecessor1 = new Room("dyn1", client, "@carol:alexandria.example.com"); + const replacedByDynamicPredecessor2 = new Room("dyn2", client, "@carol:alexandria.example.com"); + const room2 = new Room("room2", client, "@daryl:alexandria.example.com"); + client.store = new StubStore(); + client.store.getRooms = () => [ + room1, + replacedByCreate1, + replacedByCreate2, + replacedByDynamicPredecessor1, + replacedByDynamicPredecessor2, + room2, + ]; + room1.addLiveEvents( + [ + roomCreateEvent(room1.roomId, replacedByCreate1.roomId), + predecessorEvent(room1.roomId, replacedByDynamicPredecessor1.roomId), + ], + {}, + ); + room2.addLiveEvents( + [ + roomCreateEvent(room2.roomId, replacedByCreate2.roomId), + predecessorEvent(room2.roomId, replacedByDynamicPredecessor2.roomId), + ], + {}, + ); + replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {}); + replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {}); + replacedByDynamicPredecessor1.addLiveEvents( + [tombstoneEvent(room1.roomId, replacedByDynamicPredecessor1.roomId)], + {}, + ); + replacedByDynamicPredecessor2.addLiveEvents( + [tombstoneEvent(room2.roomId, replacedByDynamicPredecessor2.roomId)], + {}, + ); + + return { + room1, + room2, + replacedByCreate1, + replacedByCreate2, + replacedByDynamicPredecessor1, + replacedByDynamicPredecessor2, + }; + } it("Returns an empty list if there are no rooms", () => { client.store = new StubStore(); client.store.getRooms = () => []; @@ -2275,6 +2346,82 @@ describe("MatrixClient", function () { expect(rooms).toContain(room1); expect(rooms).toContain(room2); }); + + it("Ignores m.predecessor if we don't ask to use it", () => { + // Given 6 rooms, 2 of which have been replaced, and 2 of which WERE + // replaced by create events, but are now NOT replaced, because an + // m.predecessor event has changed the room's predecessor. + const { + room1, + room2, + replacedByCreate1, + replacedByCreate2, + replacedByDynamicPredecessor1, + replacedByDynamicPredecessor2, + } = setUpReplacedRooms(); + + // When we ask for the visible rooms + const rooms = client.getVisibleRooms(); // Don't supply msc3946ProcessDynamicPredecessor + + // Then we only get the ones that have not been replaced + expect(rooms).not.toContain(replacedByCreate1); + expect(rooms).not.toContain(replacedByCreate2); + expect(rooms).toContain(replacedByDynamicPredecessor1); + expect(rooms).toContain(replacedByDynamicPredecessor2); + expect(rooms).toContain(room1); + expect(rooms).toContain(room2); + }); + + it("Considers rooms replaced with m.predecessor events to be replaced", () => { + // Given 6 rooms, 2 of which have been replaced, and 2 of which WERE + // replaced by create events, but are now NOT replaced, because an + // m.predecessor event has changed the room's predecessor. + const { + room1, + room2, + replacedByCreate1, + replacedByCreate2, + replacedByDynamicPredecessor1, + replacedByDynamicPredecessor2, + } = setUpReplacedRooms(); + + // When we ask for the visible rooms + const useMsc3946 = true; + const rooms = client.getVisibleRooms(useMsc3946); + + // Then we only get the ones that have not been replaced + expect(rooms).not.toContain(replacedByDynamicPredecessor1); + expect(rooms).not.toContain(replacedByDynamicPredecessor2); + expect(rooms).toContain(replacedByCreate1); + expect(rooms).toContain(replacedByCreate2); + expect(rooms).toContain(room1); + expect(rooms).toContain(room2); + }); + + it("Ignores m.predecessor if we don't ask to use it", () => { + // Given 6 rooms, 2 of which have been replaced, and 2 of which WERE + // replaced by create events, but are now NOT replaced, because an + // m.predecessor event has changed the room's predecessor. + const { + room1, + room2, + replacedByCreate1, + replacedByCreate2, + replacedByDynamicPredecessor1, + replacedByDynamicPredecessor2, + } = setUpReplacedRooms(); + + // When we ask for the visible rooms + const rooms = client.getVisibleRooms(); // Don't supply msc3946ProcessDynamicPredecessor + + // Then we only get the ones that have not been replaced + expect(rooms).not.toContain(replacedByCreate1); + expect(rooms).not.toContain(replacedByCreate2); + expect(rooms).toContain(replacedByDynamicPredecessor1); + expect(rooms).toContain(replacedByDynamicPredecessor2); + expect(rooms).toContain(room1); + expect(rooms).toContain(room2); + }); }); describe("getRoomUpgradeHistory", () => { @@ -2311,6 +2458,52 @@ describe("MatrixClient", function () { return [room1, room2, room3, room4]; } + /** + * Creates 2 alternate chains of room history: one using create + * events, and one using MSC2946 predecessor+tombstone events. + * + * Using create, history looks like: + * room1->room2->room3->room4 (but note we do not create tombstones) + * + * Using predecessor+tombstone, history looks like: + * dynRoom1->dynRoom2->room3->dynRoom4->dynRoom4 + * + * @returns [room1, room2, room3, room4, dynRoom1, dynRoom2, + * dynRoom4, dynRoom5]. + */ + function createDynamicRoomHistory(): [Room, Room, Room, Room, Room, Room, Room, Room] { + // Don't create tombstones for the old versions - we generally + // expect only one tombstone in a room, and we are confused by + // anything else. + const creates = true; + const tombstones = false; + const [room1, room2, room3, room4] = createRoomHistory(creates, tombstones); + const dynRoom1 = new Room("dynRoom1", client, "@rick:grimes.example.com"); + const dynRoom2 = new Room("dynRoom2", client, "@rick:grimes.example.com"); + const dynRoom4 = new Room("dynRoom4", client, "@rick:grimes.example.com"); + const dynRoom5 = new Room("dynRoom5", client, "@rick:grimes.example.com"); + + dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], {}); + dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)]); + + dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], {}); + room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)]); + + room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], {}); + dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)]); + + dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], {}); + dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)]); + + mocked(store.getRoom) + .mockClear() + .mockImplementation((roomId: string) => { + return { room1, room2, room3, room4, dynRoom1, dynRoom2, dynRoom4, dynRoom5 }[roomId] || null; + }); + + return [room1, room2, room3, room4, dynRoom1, dynRoom2, dynRoom4, dynRoom5]; + } + it("Returns an empty list if room does not exist", () => { const history = client.getRoomUpgradeHistory("roomthatdoesnotexist"); expect(history).toHaveLength(0); @@ -2453,6 +2646,30 @@ describe("MatrixClient", function () { room4.roomId, ]); }); + + it("Returns the predecessors and subsequent rooms using MSC3945 dynamic room predecessors", () => { + const [, , room3, , dynRoom1, dynRoom2, dynRoom4, dynRoom5] = createDynamicRoomHistory(); + const useMsc3946 = true; + const verifyLinks = false; + const history = client.getRoomUpgradeHistory(room3.roomId, verifyLinks, useMsc3946); + expect(history.map((room) => room.roomId)).toEqual([ + dynRoom1.roomId, + dynRoom2.roomId, + room3.roomId, + dynRoom4.roomId, + dynRoom5.roomId, + ]); + }); + + it("When not asking for MSC3946, verified history without tombstones is empty", () => { + // There no tombstones to match the create events + const [, , room3] = createDynamicRoomHistory(); + const useMsc3946 = false; + const verifyLinks = true; + const history = client.getRoomUpgradeHistory(room3.roomId, verifyLinks, useMsc3946); + // So we get no history back + expect(history.map((room) => room.roomId)).toEqual([room3.roomId]); + }); }); }); }); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 705199ddbdd..cdef4906b02 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -3340,6 +3340,20 @@ describe("Room", function () { }); } + function predecessorEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent { + return new MatrixEvent({ + content: { + predecessor_room_id: predecessorRoomId, + }, + event_id: `predecessor_event_id_pred_${predecessorRoomId}`, + origin_server_ts: 1432735824653, + room_id: newRoomId, + sender: "@daryl:alexandria.example.com", + state_key: "", + type: "org.matrix.msc3946.room_predecessor", + }); + } + it("Returns null if there is no create event", () => { const room = new Room("roomid", client!, "@u:example.com"); expect(room.findPredecessorRoomId()).toBeNull(); @@ -3356,5 +3370,37 @@ describe("Room", function () { room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]); expect(room.findPredecessorRoomId()).toBe("replacedroomid"); }); + + it("Prefers the m.predecessor event if one exists", () => { + const room = new Room("roomid", client!, "@u:example.com"); + room.addLiveEvents([ + roomCreateEvent("roomid", "replacedroomid"), + predecessorEvent("roomid", "otherreplacedroomid"), + ]); + const useMsc3946 = true; + expect(room.findPredecessorRoomId(useMsc3946)).toBe("otherreplacedroomid"); + }); + + it("Ignores the m.predecessor event if we don't ask to use it", () => { + const room = new Room("roomid", client!, "@u:example.com"); + room.addLiveEvents([ + roomCreateEvent("roomid", "replacedroomid"), + predecessorEvent("roomid", "otherreplacedroomid"), + ]); + // Don't provide an argument for msc3946ProcessDynamicPredecessor - + // we should ignore the predecessor event. + expect(room.findPredecessorRoomId()).toBe("replacedroomid"); + }); + + it("Ignores the m.predecessor event and returns null if we don't ask to use it", () => { + const room = new Room("roomid", client!, "@u:example.com"); + room.addLiveEvents([ + roomCreateEvent("roomid", null), // Create event has no predecessor + predecessorEvent("roomid", "otherreplacedroomid"), + ]); + // Don't provide an argument for msc3946ProcessDynamicPredecessor - + // we should ignore the predecessor event. + expect(room.findPredecessorRoomId()).toBeNull(); + }); }); }); diff --git a/src/@types/event.ts b/src/@types/event.ts index 2859077218d..17af8df0272 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -33,6 +33,7 @@ export enum EventType { RoomGuestAccess = "m.room.guest_access", RoomServerAcl = "m.room.server_acl", RoomTombstone = "m.room.tombstone", + RoomPredecessor = "org.matrix.msc3946.room_predecessor", SpaceChild = "m.space.child", SpaceParent = "m.space.parent", diff --git a/src/client.ts b/src/client.ts index 869737e7425..005eefcd6d0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3780,14 +3780,18 @@ export class MatrixClient extends TypedEventEmitter { } /** + * @param msc3946ProcessDynamicPredecessor - if true, look for an + * m.room.predecessor state event and + * use it if found (MSC3946). * @returns the ID of the room that was this room's predecessor, or null if * this room has no predecessor. */ - public findPredecessorRoomId(): string | null { + public findPredecessorRoomId(msc3946ProcessDynamicPredecessor = false): string | null { const currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); if (!currentState) { return null; } + if (msc3946ProcessDynamicPredecessor) { + const predecessorEvent = currentState.getStateEvents(EventType.RoomPredecessor, ""); + if (predecessorEvent) { + const roomId = predecessorEvent.getContent()["predecessor_room_id"]; + if (roomId) { + return roomId; + } + } + } const createEvent = currentState.getStateEvents(EventType.RoomCreate, ""); if (createEvent) { From 415576d0a02d59dd6c0a808da92b36730904d562 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 26 Jan 2023 10:58:33 +0000 Subject: [PATCH 22/45] Provide eventId as well as roomId from Room.findPredecessor (#3095) --- spec/unit/room.spec.ts | 15 +++++++++------ src/client.ts | 8 ++++---- src/models/room.ts | 22 ++++++++++++++-------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index cdef4906b02..fb5aca12677 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -3356,19 +3356,19 @@ describe("Room", function () { it("Returns null if there is no create event", () => { const room = new Room("roomid", client!, "@u:example.com"); - expect(room.findPredecessorRoomId()).toBeNull(); + expect(room.findPredecessor()).toBeNull(); }); it("Returns null if the create event has no predecessor", () => { const room = new Room("roomid", client!, "@u:example.com"); room.addLiveEvents([roomCreateEvent("roomid", null)]); - expect(room.findPredecessorRoomId()).toBeNull(); + expect(room.findPredecessor()).toBeNull(); }); it("Returns the predecessor ID if one is provided via create event", () => { const room = new Room("roomid", client!, "@u:example.com"); room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]); - expect(room.findPredecessorRoomId()).toBe("replacedroomid"); + expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" }); }); it("Prefers the m.predecessor event if one exists", () => { @@ -3378,7 +3378,10 @@ describe("Room", function () { predecessorEvent("roomid", "otherreplacedroomid"), ]); const useMsc3946 = true; - expect(room.findPredecessorRoomId(useMsc3946)).toBe("otherreplacedroomid"); + expect(room.findPredecessor(useMsc3946)).toEqual({ + roomId: "otherreplacedroomid", + eventId: null, // m.predecessor does not include an event_id + }); }); it("Ignores the m.predecessor event if we don't ask to use it", () => { @@ -3389,7 +3392,7 @@ describe("Room", function () { ]); // Don't provide an argument for msc3946ProcessDynamicPredecessor - // we should ignore the predecessor event. - expect(room.findPredecessorRoomId()).toBe("replacedroomid"); + expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" }); }); it("Ignores the m.predecessor event and returns null if we don't ask to use it", () => { @@ -3400,7 +3403,7 @@ describe("Room", function () { ]); // Don't provide an argument for msc3946ProcessDynamicPredecessor - // we should ignore the predecessor event. - expect(room.findPredecessorRoomId()).toBeNull(); + expect(room.findPredecessor()).toBeNull(); }); }); }); diff --git a/src/client.ts b/src/client.ts index 005eefcd6d0..70ee1be6fcc 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3791,7 +3791,7 @@ export class MatrixClient extends TypedEventEmitter { /** * @param msc3946ProcessDynamicPredecessor - if true, look for an - * m.room.predecessor state event and - * use it if found (MSC3946). - * @returns the ID of the room that was this room's predecessor, or null if - * this room has no predecessor. - */ - public findPredecessorRoomId(msc3946ProcessDynamicPredecessor = false): string | null { + * m.room.predecessor state event and use it if found (MSC3946). + * @returns null if this room has no predecessor. Otherwise, returns + * the roomId and last eventId of the predecessor room. + * If msc3946ProcessDynamicPredecessor is true, use m.predecessor events + * as well as m.room.create events to find predecessors. + * Note: if an m.predecessor event is used, eventId is null since those + * events do not include an event_id property. + */ + public findPredecessor( + msc3946ProcessDynamicPredecessor = false, + ): { roomId: string; eventId: string | null } | null { const currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); if (!currentState) { return null; @@ -3047,7 +3052,7 @@ export class Room extends ReadReceipt { if (predecessorEvent) { const roomId = predecessorEvent.getContent()["predecessor_room_id"]; if (roomId) { - return roomId; + return { roomId, eventId: null }; } } } @@ -3058,7 +3063,8 @@ export class Room extends ReadReceipt { if (predecessor) { const roomId = predecessor["room_id"]; if (roomId) { - return roomId; + const eventId = predecessor["event_id"] || null; + return { roomId, eventId }; } } } From 5c0cb3a536fec9deb993837153d5b16e35bf12ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jan 2023 14:50:36 +0000 Subject: [PATCH 23/45] fix(deps): update all non-major dependencies (#3099) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +- yarn.lock | 176 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 114 insertions(+), 66 deletions(-) diff --git a/package.json b/package.json index 15ef53ad239..b672dd57b59 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.31.0", + "eslint": "8.32.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.2", + "prettier": "2.8.3", "rimraf": "^4.0.0", "terser": "^5.5.1", "tsify": "^5.0.2", diff --git a/yarn.lock b/yarn.lock index b8cef56d0e9..924097c143f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1399,9 +1399,9 @@ 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== + version "0.1.0-alpha.3" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.3.tgz#829ea03bcad8051dc1e4f0da18a66d4ba273f78f" + integrity sha512-KpEddjC34aobFlUYf2mIaXqkjLC0goRmYnbDZLTd0MwiFtau4b1TrPptQ8XFc90Z2VeAcvf18CBqA2otmZzUKQ== "@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" @@ -2043,7 +2043,7 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -array-includes@^3.1.4: +array-includes@^3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== @@ -2059,7 +2059,7 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.flat@^1.2.5: +array.prototype.flat@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== @@ -2069,6 +2069,16 @@ array.prototype.flat@^1.2.5: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" +array.prototype.flatmap@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" + integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + asap@~2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -2943,13 +2953,6 @@ debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" -debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -3174,26 +3177,32 @@ 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.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.5.tgz#e6dc99177be37cacda5988e692c3fa8b218e95d2" - integrity sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ== + version "1.21.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6" + integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg== dependencies: + available-typed-arrays "^1.0.5" call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" es-to-primitive "^1.2.1" function-bind "^1.1.1" function.prototype.name "^1.1.5" get-intrinsic "^1.1.3" get-symbol-description "^1.0.0" + globalthis "^1.0.3" gopd "^1.0.1" has "^1.0.3" has-property-descriptors "^1.0.0" + has-proto "^1.0.1" has-symbols "^1.0.3" - internal-slot "^1.0.3" + internal-slot "^1.0.4" + is-array-buffer "^3.0.1" is-callable "^1.2.7" is-negative-zero "^2.0.2" is-regex "^1.1.4" is-shared-array-buffer "^1.0.2" is-string "^1.0.7" + is-typed-array "^1.1.10" is-weakref "^1.0.2" object-inspect "^1.12.2" object-keys "^1.1.1" @@ -3202,7 +3211,18 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: safe-regex-test "^1.0.0" string.prototype.trimend "^1.0.6" string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" + +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" es-shim-unscopables@^1.0.0: version "1.0.0" @@ -3298,18 +3318,19 @@ eslint-config-prettier@^8.5.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" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" - integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== +eslint-import-resolver-node@^0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7" + integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA== dependencies: debug "^3.2.7" - resolve "^1.20.0" + is-core-module "^2.11.0" + resolve "^1.22.1" eslint-import-resolver-typescript@^3.5.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.2.tgz#9431acded7d898fd94591a08ea9eec3514c7de91" - integrity sha512-zX4ebnnyXiykjhcBvKIf5TNvt8K7yX6bllTRZ14MiurKPjDpCAZujlszTdB8pcNXhZcOf+god4s9SjQa5GnytQ== + version "3.5.3" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.3.tgz#db5ed9e906651b7a59dd84870aaef0e78c663a05" + integrity sha512-njRcKYBc3isE42LaTcJNVANR3R99H9bAxBDMNDr2W7yq5gYPxbU3MkdhsQukxZ/Xg9C2vcyLlDsbKfRDg0QvCQ== dependencies: debug "^4.3.4" enhanced-resolve "^5.10.0" @@ -3319,7 +3340,7 @@ eslint-import-resolver-typescript@^3.5.1: is-glob "^4.0.3" synckit "^0.8.4" -eslint-module-utils@^2.7.3: +eslint-module-utils@^2.7.4: version "2.7.4" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== @@ -3327,28 +3348,30 @@ eslint-module-utils@^2.7.3: debug "^3.2.7" eslint-plugin-import@^2.26.0: - version "2.26.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b" - integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== + version "2.27.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" + integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== dependencies: - array-includes "^3.1.4" - array.prototype.flat "^1.2.5" - debug "^2.6.9" + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + array.prototype.flatmap "^1.3.1" + debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.3" + eslint-import-resolver-node "^0.3.7" + eslint-module-utils "^2.7.4" has "^1.0.3" - is-core-module "^2.8.1" + is-core-module "^2.11.0" is-glob "^4.0.3" minimatch "^3.1.2" - object.values "^1.1.5" - resolve "^1.22.0" + object.values "^1.1.6" + resolve "^1.22.1" + semver "^6.3.0" tsconfig-paths "^3.14.1" eslint-plugin-jsdoc@^39.6.4: - version "39.6.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.6.4.tgz#b940aebd3eea26884a0d341785d2dc3aba6a38a7" - integrity sha512-fskvdLCfwmPjHb6e+xNGDtGgbF8X7cDwMtVLAP2WwSf9Htrx68OAx31BESBM1FAwsN2HTQyYQq7m4aW4Q4Nlag== + version "39.6.8" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.6.8.tgz#a311dbaad00e51fe835c28e14e883c19e0d528a7" + integrity sha512-8W2B2vCfqXmV6AxhEO9u25zPqk7V/LxCsZBl0xDF1CSLDqabiQQtZXpWp19K53HMfFZMLeNRJRUFFhauWgMZrA== dependencies: "@es-joy/jsdoccomment" "~0.36.1" comment-parser "1.3.1" @@ -3431,10 +3454,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.31.0: - version "8.31.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.31.0.tgz#75028e77cbcff102a9feae1d718135931532d524" - integrity sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA== +eslint@8.32.0: + version "8.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.32.0.tgz#d9690056bb6f1a302bd991e7090f5b68fbaea861" + integrity sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ== dependencies: "@eslint/eslintrc" "^1.4.1" "@humanwhocodes/config-array" "^0.11.8" @@ -3805,9 +3828,9 @@ get-caller-file@^2.0.5: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" - integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" + integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== dependencies: function-bind "^1.1.1" has "^1.0.3" @@ -3874,6 +3897,13 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + globalyzer@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" @@ -3946,6 +3976,11 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -4134,7 +4169,7 @@ insert-module-globals@^7.2.1: undeclared-identifiers "^1.1.2" xtend "^4.0.0" -internal-slot@^1.0.3: +internal-slot@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== @@ -4151,6 +4186,15 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-array-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" + integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -4195,7 +4239,7 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.1.0, is-core-module@^2.10.0, is-core-module@^2.8.1, is-core-module@^2.9.0: +is-core-module@^2.1.0, is-core-module@^2.10.0, is-core-module@^2.11.0, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== @@ -4329,7 +4373,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typed-array@^1.1.10, is-typed-array@^1.1.3: +is-typed-array@^1.1.10, is-typed-array@^1.1.3, is-typed-array@^1.1.9: version "1.1.10" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== @@ -5325,11 +5369,6 @@ mold-source-map@^0.4.0: convert-source-map "^1.1.0" through "~2.2.7" -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -5417,9 +5456,9 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-inspect@^1.12.2, object-inspect@^1.9.0: - version "1.12.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" - integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== object-keys@^1.1.1: version "1.1.1" @@ -5436,7 +5475,7 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.values@^1.1.5: +object.values@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== @@ -5699,10 +5738,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.2: - version "2.8.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.2.tgz#c4ea1b5b454d7c4b59966db2e06ed7eec5dfd160" - integrity sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw== +prettier@2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.3.tgz#ab697b1d3dd46fb4626fbe2f543afe0cc98d8632" + integrity sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw== pretty-format@^28.1.3: version "28.1.3" @@ -6164,7 +6203,7 @@ resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@^1.1.4, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.4.0: +resolve@^1.1.4, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.4.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -6857,6 +6896,15 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -7199,7 +7247,7 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" -which-typed-array@^1.1.2: +which-typed-array@^1.1.2, which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== From c9bc20aa4d2c10d12efeeedace41b7a4e87dd6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 26 Jan 2023 17:28:56 +0100 Subject: [PATCH 24/45] Don't throw with no `opponentDeviceInfo` (#3107) --- src/webrtc/call.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index e0c8310a338..4ddc2edf343 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -2335,11 +2335,16 @@ export class MatrixCall extends TypedEventEmitter Date: Mon, 30 Jan 2023 11:25:27 +0000 Subject: [PATCH 25/45] Stop labelling threads as experimental (#3064) --- .../matrix-client-event-timeline.spec.ts | 28 +++++------ spec/integ/matrix-client-methods.spec.ts | 20 ++++---- spec/test-utils/webrtc.ts | 4 +- spec/unit/event-timeline-set.spec.ts | 8 ++-- spec/unit/matrix-client.spec.ts | 24 +++++++--- spec/unit/models/thread.spec.ts | 4 +- spec/unit/notifications.spec.ts | 2 +- spec/unit/room.spec.ts | 18 +++---- src/client.ts | 48 ++++++++++++++++--- src/models/event.ts | 11 ++--- src/models/room.ts | 23 ++++----- src/models/thread.ts | 3 -- src/sync.ts | 4 +- 13 files changed, 118 insertions(+), 79 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 0c553e77ff5..dbcb48acda7 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.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. @@ -590,7 +590,7 @@ describe("MatrixClient event timelines", function () { it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => { // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); await client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId)!; @@ -647,7 +647,7 @@ describe("MatrixClient event timelines", function () { it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => { // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId)!; @@ -680,7 +680,7 @@ describe("MatrixClient event timelines", function () { it("should return undefined when event is not in the thread that the given timelineSet is representing", () => { // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId)!; @@ -709,7 +709,7 @@ describe("MatrixClient event timelines", function () { it("should return undefined when event is within a thread but timelineSet is not", () => { // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId)!; @@ -1127,7 +1127,7 @@ describe("MatrixClient event timelines", function () { }; // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Stable); Thread.setServerSideListSupport(FeatureSupport.Stable); Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable); @@ -1263,7 +1263,7 @@ describe("MatrixClient event timelines", function () { describe("with server compatibility", function () { beforeEach(() => { // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Stable); Thread.setServerSideListSupport(FeatureSupport.Stable); Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable); @@ -1421,7 +1421,7 @@ describe("MatrixClient event timelines", function () { }; // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Stable); Thread.setServerSideListSupport(FeatureSupport.Stable); Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable); @@ -1473,7 +1473,7 @@ describe("MatrixClient event timelines", function () { describe("without server compatibility", function () { beforeEach(() => { // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); Thread.setServerSideListSupport(FeatureSupport.None); }); @@ -1539,7 +1539,7 @@ describe("MatrixClient event timelines", function () { it("should add lazy loading filter", async () => { // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); Thread.setServerSideListSupport(FeatureSupport.Stable); // @ts-ignore @@ -1567,7 +1567,7 @@ describe("MatrixClient event timelines", function () { it("should correctly pass pagination token", async () => { // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); Thread.setServerSideListSupport(FeatureSupport.Stable); @@ -1892,7 +1892,7 @@ describe("MatrixClient event timelines", function () { it("in stable mode", async () => { // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Stable); Thread.setServerSideListSupport(FeatureSupport.Stable); Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable); @@ -1902,7 +1902,7 @@ describe("MatrixClient event timelines", function () { it("in backwards compatible unstable mode", async () => { // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); Thread.setServerSideListSupport(FeatureSupport.Experimental); Thread.setServerSideFwdPaginationSupport(FeatureSupport.Experimental); @@ -1912,7 +1912,7 @@ describe("MatrixClient event timelines", function () { it("in backwards compatible mode", async () => { // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; + client.clientOpts.threadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); Thread.setServerSideListSupport(FeatureSupport.None); Thread.setServerSideFwdPaginationSupport(FeatureSupport.None); diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index 01e24ded4ea..c5ea7fe5d98 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.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. @@ -35,7 +35,7 @@ describe("MatrixClient", function () { let store: MemoryStore | undefined; const defaultClientOpts: IStoredClientOpts = { - experimentalThreadSupport: false, + threadSupport: false, }; const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => { const store = new MemoryStore(); @@ -671,7 +671,7 @@ describe("MatrixClient", function () { // @ts-ignore setting private property client!.clientOpts = { ...defaultClientOpts, - experimentalThreadSupport: true, + threadSupport: true, }; const eventPollResponseReference = buildEventPollResponseReference(); @@ -702,7 +702,7 @@ describe("MatrixClient", function () { // @ts-ignore setting private property client!.clientOpts = { ...defaultClientOpts, - experimentalThreadSupport: true, + threadSupport: true, }; const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); @@ -726,7 +726,7 @@ describe("MatrixClient", function () { // @ts-ignore setting private property client!.clientOpts = { ...defaultClientOpts, - experimentalThreadSupport: true, + threadSupport: true, }; const eventPollResponseReference = buildEventPollResponseReference(); @@ -750,7 +750,7 @@ describe("MatrixClient", function () { // @ts-ignore setting private property client!.clientOpts = { ...defaultClientOpts, - experimentalThreadSupport: true, + threadSupport: true, }; const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); @@ -774,7 +774,7 @@ describe("MatrixClient", function () { // @ts-ignore setting private property client!.clientOpts = { ...defaultClientOpts, - experimentalThreadSupport: true, + threadSupport: true, }; // This is based on recording the events in a real room: @@ -831,7 +831,7 @@ describe("MatrixClient", function () { // @ts-ignore setting private property client!.clientOpts = { ...defaultClientOpts, - experimentalThreadSupport: true, + threadSupport: true, }; const threadRootEvent = buildEventPollStartThreadRoot(); @@ -857,7 +857,7 @@ describe("MatrixClient", function () { // @ts-ignore setting private property client!.clientOpts = { ...defaultClientOpts, - experimentalThreadSupport: true, + threadSupport: true, }; const threadRootEvent = buildEventPollStartThreadRoot(); @@ -878,7 +878,7 @@ describe("MatrixClient", function () { // @ts-ignore setting private property client!.clientOpts = { ...defaultClientOpts, - experimentalThreadSupport: true, + threadSupport: true, }; const threadRootEvent = buildEventPollStartThreadRoot(); diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index ed6e408ab1f..8bc8825a4dc 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.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. @@ -476,7 +476,7 @@ export class MockCallMatrixClient extends TypedEventEmitter().mockReturnValue([]); public getRoom = jest.fn(); - public supportsExperimentalThreads(): boolean { + public supportsThreads(): boolean { return true; } public async decryptEventIfNeeded(): Promise {} diff --git a/spec/unit/event-timeline-set.spec.ts b/spec/unit/event-timeline-set.spec.ts index 05b80fe57de..6322289f059 100644 --- a/spec/unit/event-timeline-set.spec.ts +++ b/spec/unit/event-timeline-set.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. @@ -147,7 +147,7 @@ describe("EventTimelineSet", () => { let thread: Thread; beforeEach(() => { - (client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true); + (client.supportsThreads as jest.Mock).mockReturnValue(true); thread = new Thread("!thread_id:server", messageEvent, { room, client }); }); @@ -206,7 +206,7 @@ describe("EventTimelineSet", () => { }); it("should allow edits to be added to thread timeline", async () => { - jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true); + jest.spyOn(client, "supportsThreads").mockReturnValue(true); jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {})); Thread.hasServerSideSupport = FeatureSupport.Stable; @@ -393,7 +393,7 @@ describe("EventTimelineSet", () => { let thread: Thread; beforeEach(() => { - (client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true); + (client.supportsThreads as jest.Mock).mockReturnValue(true); thread = new Thread("!thread_id:server", messageEvent, { room, client }); }); diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 24d3fed7727..e78d2eab19a 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.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. @@ -17,7 +17,7 @@ limitations under the License. import { mocked } from "jest-mock"; import { logger } from "../../src/logger"; -import { ClientEvent, ITurnServerResponse, MatrixClient, Store } from "../../src/client"; +import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client"; import { Filter } from "../../src/filter"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace"; import { @@ -276,7 +276,7 @@ describe("MatrixClient", function () { ); } - function makeClient() { + function makeClient(opts?: Partial) { client = new MatrixClient({ baseUrl: "https://my.home.server", idBaseUrl: identityServerUrl, @@ -285,6 +285,7 @@ describe("MatrixClient", function () { store: store, scheduler: scheduler, userId: userId, + ...(opts || {}), }); // FIXME: We shouldn't be yanking http like this. client.http = (["authedRequest", "getContentUri", "request", "uploadContent"] as const).reduce((r, k) => { @@ -1456,9 +1457,20 @@ describe("MatrixClient", function () { }); describe("threads", () => { + it.each([ + { startOpts: {}, hasThreadSupport: false }, + { startOpts: { threadSupport: true }, hasThreadSupport: true }, + { startOpts: { threadSupport: false }, hasThreadSupport: false }, + { startOpts: { experimentalThreadSupport: true }, hasThreadSupport: true }, + { startOpts: { experimentalThreadSupport: true, threadSupport: false }, hasThreadSupport: false }, + ])("enabled thread support for the SDK instance ", async ({ startOpts, hasThreadSupport }) => { + await client.startClient(startOpts); + expect(client.supportsThreads()).toBe(hasThreadSupport); + }); + it("partitions root events to room timeline and thread timeline", () => { - const supportsExperimentalThreads = client.supportsExperimentalThreads; - client.supportsExperimentalThreads = () => true; + const supportsThreads = client.supportsThreads; + client.supportsThreads = () => true; const room = new Room("!room1:matrix.org", client, userId); const rootEvent = new MatrixEvent({ @@ -1487,7 +1499,7 @@ describe("MatrixClient", function () { expect(threadEvents).toHaveLength(1); // Restore method - client.supportsExperimentalThreads = supportsExperimentalThreads; + client.supportsThreads = supportsThreads; }); }); diff --git a/spec/unit/models/thread.spec.ts b/spec/unit/models/thread.spec.ts index 4dd5a681b6e..0583bf391b3 100644 --- a/spec/unit/models/thread.spec.ts +++ b/spec/unit/models/thread.spec.ts @@ -84,7 +84,7 @@ describe("Thread", () => { ...mockClientMethodsUser(), getRoom: jest.fn().mockImplementation(() => room), decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0), - supportsExperimentalThreads: jest.fn().mockReturnValue(true), + supportsThreads: jest.fn().mockReturnValue(true), }); client.reEmitter = mock(ReEmitter, "ReEmitter"); client.canSupport = new Map(); @@ -195,7 +195,7 @@ describe("Thread", () => { ...mockClientMethodsUser(), getRoom: jest.fn().mockImplementation(() => room), decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0), - supportsExperimentalThreads: jest.fn().mockReturnValue(true), + supportsThreads: jest.fn().mockReturnValue(true), }); client.reEmitter = mock(ReEmitter, "ReEmitter"); client.canSupport = new Map(); diff --git a/spec/unit/notifications.spec.ts b/spec/unit/notifications.spec.ts index 594740e285a..aefe5777a53 100644 --- a/spec/unit/notifications.spec.ts +++ b/spec/unit/notifications.spec.ts @@ -56,7 +56,7 @@ describe("fixNotificationCountOnDecryption", () => { getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)), getRoom: jest.fn().mockImplementation(() => room), decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0), - supportsExperimentalThreads: jest.fn().mockReturnValue(true), + supportsThreads: jest.fn().mockReturnValue(true), }); mockClient.reEmitter = mock(ReEmitter, "ReEmitter"); mockClient.canSupport = new Map(); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index fb5aca12677..83ae123ffdb 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2022, 2023 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. @@ -1624,7 +1624,7 @@ describe("Room", function () { describe("addPendingEvent", function () { it("should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'", function () { const client = new TestClient("@alice:example.com", "alicedevice").client; - client.supportsExperimentalThreads = () => true; + client.supportsThreads = () => true; const room = new Room(roomId, client, userA, { pendingEventOrdering: PendingEventOrdering.Detached, }); @@ -2470,7 +2470,7 @@ describe("Room", function () { }); it("Edits update the lastReply event", async () => { - room.client.supportsExperimentalThreads = () => true; + room.client.supportsThreads = () => true; Thread.setServerSideSupport(FeatureSupport.Stable); const randomMessage = mkMessage(); @@ -2541,7 +2541,7 @@ describe("Room", function () { }); it("Redactions to thread responses decrement the length", async () => { - room.client.supportsExperimentalThreads = () => true; + room.client.supportsThreads = () => true; Thread.setServerSideSupport(FeatureSupport.Stable); const threadRoot = mkMessage(); @@ -2608,7 +2608,7 @@ describe("Room", function () { }); it("Redactions to reactions in threads do not decrement the length", async () => { - room.client.supportsExperimentalThreads = () => true; + room.client.supportsThreads = () => true; Thread.setServerSideSupport(FeatureSupport.Stable); const threadRoot = mkMessage(); @@ -2648,7 +2648,7 @@ describe("Room", function () { }); it("should not decrement the length when the thread root is redacted", async () => { - room.client.supportsExperimentalThreads = () => true; + room.client.supportsThreads = () => true; Thread.setServerSideSupport(FeatureSupport.Stable); const threadRoot = mkMessage(); @@ -2689,7 +2689,7 @@ describe("Room", function () { }); it("Redacting the lastEvent finds a new lastEvent", async () => { - room.client.supportsExperimentalThreads = () => true; + room.client.supportsThreads = () => true; Thread.setServerSideSupport(FeatureSupport.Stable); Thread.setServerSideListSupport(FeatureSupport.Stable); @@ -2796,7 +2796,7 @@ describe("Room", function () { describe("eventShouldLiveIn", () => { const client = new TestClient(userA).client; - client.supportsExperimentalThreads = () => true; + client.supportsThreads = () => true; Thread.setServerSideSupport(FeatureSupport.Stable); const room = new Room(roomId, client, userA); @@ -3307,7 +3307,7 @@ describe("Room", function () { beforeEach(() => { client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(), - supportsExperimentalThreads: jest.fn().mockReturnValue(true), + supportsThreads: jest.fn().mockReturnValue(true), }); }); diff --git a/src/client.ts b/src/client.ts index 70ee1be6fcc..22a0a1d068f 100644 --- a/src/client.ts +++ b/src/client.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. @@ -446,10 +446,16 @@ export interface IStartClientOpts { clientWellKnownPollPeriod?: number; /** - * @experimental + * @deprecated use `threadSupport` instead */ experimentalThreadSupport?: boolean; + /** + * Will organises events in threaded conversations when + * a thread relation is encountered + */ + threadSupport?: boolean; + /** * @experimental */ @@ -1448,6 +1454,19 @@ export class MatrixClient extends TypedEventEmitter { - if (!this.supportsExperimentalThreads()) { + if (!this.supportsThreads()) { throw new Error("could not get thread timeline: no client support"); } @@ -9337,12 +9356,21 @@ export class MatrixClient extends TypedEventEmitter(THREAD_RELATION_TYPE.name); @@ -1557,7 +1555,8 @@ export class MatrixEvent extends TypedEventEmitter { public readonly relations = new RelationsContainer(this.client, this); /** - * @experimental + * A collection of events known by the client + * This is not a comprehensive list of the threads that exist in this room */ private threads = new Map(); public lastThread?: Thread; @@ -483,7 +484,7 @@ export class Room extends ReadReceipt { return this.threadTimelineSetsPromise; } - if (this.client?.supportsExperimentalThreads()) { + if (this.client?.supportsThreads()) { try { this.threadTimelineSetsPromise = Promise.all([ this.createThreadTimelineSet(), @@ -1330,7 +1331,6 @@ export class Room extends ReadReceipt { } /** - * @experimental * Get one of the notification counts for a thread * @param threadId - the root event ID * @param type - The type of notification count to get. default: 'total' @@ -1342,7 +1342,6 @@ export class Room extends ReadReceipt { } /** - * @experimental * Checks if the current room has unread thread notifications * @returns */ @@ -1356,7 +1355,6 @@ export class Room extends ReadReceipt { } /** - * @experimental * Swet one of the notification count for a thread * @param threadId - the root event ID * @param type - The type of notification count to get. default: 'total' @@ -1377,7 +1375,6 @@ export class Room extends ReadReceipt { } /** - * @experimental * @returns the notification count type for all the threads in the room */ public get threadsAggregateNotificationType(): NotificationCountType | null { @@ -1393,7 +1390,6 @@ export class Room extends ReadReceipt { } /** - * @experimental * Resets the thread notifications for this room */ public resetThreadUnreadNotificationCount(notificationsToKeep?: string[]): void { @@ -1553,14 +1549,16 @@ export class Room extends ReadReceipt { } /** - * @experimental + * Get the instance of the thread associated with the current event + * @param eventId - the ID of the current event + * @returns a thread instance if known */ public getThread(eventId: string): Thread | null { return this.threads.get(eventId) ?? null; } /** - * @experimental + * Get all the known threads in the room */ public getThreads(): Thread[] { return Array.from(this.threads.values()); @@ -1827,7 +1825,7 @@ export class Room extends ReadReceipt { * Without server support that means fetching as much at once as the server allows us to. */ public async fetchRoomThreads(): Promise { - if (this.threadsReady || !this.client.supportsExperimentalThreads()) { + if (this.threadsReady || !this.client.supportsThreads()) { return; } @@ -2004,7 +2002,7 @@ export class Room extends ReadReceipt { shouldLiveInThread: boolean; threadId?: string; } { - if (!this.client?.supportsExperimentalThreads()) { + if (!this.client?.supportsThreads()) { return { shouldLiveInRoom: true, shouldLiveInThread: false, @@ -2073,7 +2071,6 @@ export class Room extends ReadReceipt { /** * Adds events to a thread's timeline. Will fire "Thread.update" - * @experimental */ public processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void { events.forEach(this.applyRedaction); @@ -2702,7 +2699,7 @@ export class Room extends ReadReceipt { // Indices to the events array, for readability const ROOM = 0; const THREAD = 1; - if (this.client.supportsExperimentalThreads()) { + if (this.client.supportsThreads()) { const threadRoots = this.findThreadRoots(events); return events.reduce( (memo, event: MatrixEvent) => { diff --git a/src/models/thread.ts b/src/models/thread.ts index b688d12ac11..e6420a659f7 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -69,9 +69,6 @@ export function determineFeatureSupport(stable: boolean, unstable: boolean): Fea } } -/** - * @experimental - */ export class Thread extends ReadReceipt { public static hasServerSideSupport = FeatureSupport.None; public static hasServerSideListSupport = FeatureSupport.None; diff --git a/src/sync.ts b/src/sync.ts index 11fe3cc102b..81892e64f12 100644 --- a/src/sync.ts +++ b/src/sync.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. @@ -198,7 +198,7 @@ export function defaultClientOpts(opts?: IStoredClientOpts): IStoredClientOpts { resolveInvitesToProfiles: false, pollTimeout: 30 * 1000, pendingEventOrdering: PendingEventOrdering.Chronological, - experimentalThreadSupport: false, + threadSupport: false, ...opts, }; } From 4f918f684e9d11d609c3b7c11133414b0ca58fde Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 30 Jan 2023 09:26:43 -0500 Subject: [PATCH 26/45] add support for stable identifier for fixed MAC in SAS verification (#3101) --- spec/unit/crypto/verification/sas.spec.ts | 2 +- src/crypto/verification/SAS.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/spec/unit/crypto/verification/sas.spec.ts b/spec/unit/crypto/verification/sas.spec.ts index 5309c0d80f7..3c4f224428e 100644 --- a/spec/unit/crypto/verification/sas.spec.ts +++ b/spec/unit/crypto/verification/sas.spec.ts @@ -215,7 +215,7 @@ describe("SAS verification", function () { ]); // make sure that it uses the preferred method - expect(macMethod).toBe("org.matrix.msc3783.hkdf-hmac-sha256"); + expect(macMethod).toBe("hkdf-hmac-sha256.v2"); expect(keyAgreement).toBe("curve25519-hkdf-sha256"); // make sure Alice and Bob verified each other diff --git a/src/crypto/verification/SAS.ts b/src/crypto/verification/SAS.ts index 4c05f53549d..a8d237d2da1 100644 --- a/src/crypto/verification/SAS.ts +++ b/src/crypto/verification/SAS.ts @@ -159,6 +159,7 @@ function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas { const macMethods = { "hkdf-hmac-sha256": "calculate_mac", "org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64", + "hkdf-hmac-sha256.v2": "calculate_mac_fixed_base64", "hmac-sha256": "calculate_mac_long_kdf", } as const; @@ -202,7 +203,12 @@ type KeyAgreement = keyof typeof calculateKeyAgreement; */ const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; const HASHES_LIST = ["sha256"]; -const MAC_LIST: MacMethod[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"]; +const MAC_LIST: MacMethod[] = [ + "hkdf-hmac-sha256.v2", + "org.matrix.msc3783.hkdf-hmac-sha256", + "hkdf-hmac-sha256", + "hmac-sha256", +]; const SAS_LIST = Object.keys(sasGenerators); const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST); From 1c26dc02339c6b7c67b030b1701248a2d68c24c0 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 31 Jan 2023 10:47:39 +0000 Subject: [PATCH 27/45] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1786e9467d0..e77cdb2ac02 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "keywords": [ "matrix-org" ], - "main": "./lib/index.js", + "main": "./src/index.ts", "browser": "./src/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 0c1d5f6b255573e2f2d36045bd57a8890dc10af6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 31 Jan 2023 15:44:14 +0000 Subject: [PATCH 28/45] Element-R: implement remaining OutgoingMessage request types (#3083) This is a follow-up to #3019: it implements the remaining two types of message types, now that rust SDK has sensibly-shaped types for them. --- package.json | 2 +- .../OutgoingRequestProcessor.spec.ts | 180 ++++++++++++++++++ .../{ => rust-crypto}/rust-crypto.spec.ts | 139 +++++--------- src/rust-crypto/OutgoingRequestProcessor.ts | 109 +++++++++++ src/rust-crypto/rust-crypto.ts | 77 +------- yarn.lock | 2 +- 6 files changed, 347 insertions(+), 162 deletions(-) create mode 100644 spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts rename spec/unit/{ => rust-crypto}/rust-crypto.spec.ts (59%) create mode 100644 src/rust-crypto/OutgoingRequestProcessor.ts diff --git a/package.json b/package.json index e77cdb2ac02..88668ba5781 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.2", + "@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.3", "another-json": "^0.2.0", "bs58": "^5.0.0", "content-type": "^1.0.4", diff --git a/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts b/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts new file mode 100644 index 00000000000..28d1e9b3508 --- /dev/null +++ b/spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts @@ -0,0 +1,180 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import MockHttpBackend from "matrix-mock-request"; +import { Mocked } from "jest-mock"; +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; +import { + KeysBackupRequest, + KeysClaimRequest, + KeysQueryRequest, + KeysUploadRequest, + RoomMessageRequest, + SignatureUploadRequest, + ToDeviceRequest, +} from "@matrix-org/matrix-sdk-crypto-js"; + +import { TypedEventEmitter } from "../../../src/models/typed-event-emitter"; +import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src"; +import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; + +describe("OutgoingRequestProcessor", () => { + /** the OutgoingRequestProcessor implementation under test */ + let processor: OutgoingRequestProcessor; + + /** A mock http backend which processor is connected to */ + let httpBackend: MockHttpBackend; + + /** a mocked-up OlmMachine which processor is connected to */ + let olmMachine: Mocked; + + /** 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(); + + const dummyEventEmitter = new TypedEventEmitter(); + const httpApi = new MatrixHttpApi(dummyEventEmitter, { + baseUrl: "https://example.com", + prefix: "/_matrix", + fetchFn: httpBackend.fetchFn as typeof global.fetch, + onlyData: true, + }); + + olmMachine = { + markRequestAsSent: jest.fn(), + } as unknown as Mocked; + + processor = new OutgoingRequestProcessor(olmMachine, httpApi); + }); + + /* simple requests that map directly to the request body */ + const tests: Array<[string, any, "POST" | "PUT", string]> = [ + ["KeysUploadRequest", KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"], + ["KeysQueryRequest", KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"], + ["KeysClaimRequest", KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"], + [ + "SignatureUploadRequest", + SignatureUploadRequest, + "POST", + "https://example.com/_matrix/client/v3/keys/signatures/upload", + ], + ["KeysBackupRequest", KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"], + ]; + + test.each(tests)(`should handle %ss`, async (_, RequestClass, expectedMethod, expectedPath) => { + // first, mock up a request as we might expect to receive it from the Rust layer ... + const testBody = '{ "foo": "bar" }'; + const outgoingRequest = new RequestClass("1234", testBody); + + // ... then poke it into the OutgoingRequestProcessor under test. + const reqProm = processor.makeOutgoingRequest(outgoingRequest); + + // Now: check that it makes a matching HTTP request ... + 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); + + // ... and that it calls OlmMachine.markAsSent. + const markSentCallPromise = awaitCallToMarkAsSent(); + await httpBackend.flushAllExpected(); + + await Promise.all([reqProm, markSentCallPromise]); + expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse); + httpBackend.verifyNoOutstandingRequests(); + }); + + it("should handle ToDeviceRequests", async () => { + // first, mock up the ToDeviceRequest as we might expect to receive it from the Rust layer ... + const testBody = '{ "foo": "bar" }'; + const outgoingRequest = new ToDeviceRequest("1234", "test/type", "test/txnid", testBody); + + // ... then poke it into the OutgoingRequestProcessor under test. + const reqProm = processor.makeOutgoingRequest(outgoingRequest); + + // Now: check that it makes a matching HTTP request ... + const testResponse = '{ "result": 1 }'; + httpBackend + .when("PUT", "/_matrix") + .check((req) => { + expect(req.path).toEqual("https://example.com/_matrix/client/v3/sendToDevice/test%2Ftype/test%2Ftxnid"); + expect(req.rawData).toEqual(testBody); + expect(req.headers["Accept"]).toEqual("application/json"); + expect(req.headers["Content-Type"]).toEqual("application/json"); + }) + .respond(200, testResponse, true); + + // ... and that it calls OlmMachine.markAsSent. + const markSentCallPromise = awaitCallToMarkAsSent(); + await httpBackend.flushAllExpected(); + + await Promise.all([reqProm, markSentCallPromise]); + expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse); + httpBackend.verifyNoOutstandingRequests(); + }); + + it("should handle RoomMessageRequests", async () => { + // first, mock up the RoomMessageRequest as we might expect to receive it from the Rust layer ... + const testBody = '{ "foo": "bar" }'; + const outgoingRequest = new RoomMessageRequest("1234", "test/room", "test/txnid", "test/type", testBody); + + // ... then poke it into the OutgoingRequestProcessor under test. + const reqProm = processor.makeOutgoingRequest(outgoingRequest); + + // Now: check that it makes a matching HTTP request ... + const testResponse = '{ "result": 1 }'; + httpBackend + .when("PUT", "/_matrix") + .check((req) => { + expect(req.path).toEqual( + "https://example.com/_matrix/client/v3/room/test%2Froom/send/test%2Ftype/test%2Ftxnid", + ); + expect(req.rawData).toEqual(testBody); + expect(req.headers["Accept"]).toEqual("application/json"); + expect(req.headers["Content-Type"]).toEqual("application/json"); + }) + .respond(200, testResponse, true); + + // ... and that it calls OlmMachine.markAsSent. + const markSentCallPromise = awaitCallToMarkAsSent(); + await httpBackend.flushAllExpected(); + + await Promise.all([reqProm, markSentCallPromise]); + expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse); + httpBackend.verifyNoOutstandingRequests(); + }); + + it("does not explode with unknown requests", async () => { + const outgoingRequest = { id: "5678", type: 987 }; + const markSentCallPromise = awaitCallToMarkAsSent(); + await Promise.all([processor.makeOutgoingRequest(outgoingRequest), markSentCallPromise]); + expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, ""); + }); +}); diff --git a/spec/unit/rust-crypto.spec.ts b/spec/unit/rust-crypto/rust-crypto.spec.ts similarity index 59% rename from spec/unit/rust-crypto.spec.ts rename to spec/unit/rust-crypto/rust-crypto.spec.ts index 50e2c856caa..1a00acddfae 100644 --- a/spec/unit/rust-crypto.spec.ts +++ b/spec/unit/rust-crypto/rust-crypto.spec.ts @@ -17,24 +17,16 @@ limitations under the License. import "fake-indexeddb/auto"; import { IDBFactory } from "fake-indexeddb"; import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; -import { - KeysBackupRequest, - KeysClaimRequest, - KeysQueryRequest, - KeysUploadRequest, - OlmMachine, - SignatureUploadRequest, -} from "@matrix-org/matrix-sdk-crypto-js"; +import { KeysQueryRequest, OlmMachine } from "@matrix-org/matrix-sdk-crypto-js"; import { Mocked } from "jest-mock"; -import MockHttpBackend from "matrix-mock-request"; -import { RustCrypto } from "../../src/rust-crypto/rust-crypto"; -import { initRustCrypto } from "../../src/rust-crypto"; -import { HttpApiEvent, HttpApiEventHandlerMap, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../src"; -import { TypedEventEmitter } from "../../src/models/typed-event-emitter"; -import { mkEvent } from "../test-utils/test-utils"; -import { CryptoBackend } from "../../src/common-crypto/CryptoBackend"; -import { IEventDecryptionResult } from "../../src/@types/crypto"; +import { RustCrypto } from "../../../src/rust-crypto/rust-crypto"; +import { initRustCrypto } from "../../../src/rust-crypto"; +import { IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src"; +import { mkEvent } from "../../test-utils/test-utils"; +import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend"; +import { IEventDecryptionResult } from "../../../src/@types/crypto"; +import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; afterEach(() => { // reset fake-indexeddb after each test, to make sure we don't leak connections @@ -106,8 +98,8 @@ describe("RustCrypto", () => { /** the RustCrypto implementation under test */ let rustCrypto: RustCrypto; - /** A mock http backend which rustCrypto is connected to */ - let httpBackend: MockHttpBackend; + /** A mock OutgoingRequestProcessor which rustCrypto is connected to */ + let outgoingRequestProcessor: Mocked; /** a mocked-up OlmMachine which rustCrypto is connected to */ let olmMachine: Mocked; @@ -116,28 +108,25 @@ describe("RustCrypto", () => { * 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); + /** wait for a call to outgoingRequestProcessor.makeOutgoingRequest. + * + * The promise resolves to a callback: the makeOutgoingRequest call will not complete until the returned + * callback is called. + */ + function awaitCallToMakeOutgoingRequest(): Promise<() => void> { + return new Promise<() => void>((resolveCalledPromise, _reject) => { + outgoingRequestProcessor.makeOutgoingRequest.mockImplementationOnce(async () => { + const completePromise = new Promise((resolveCompletePromise, _reject) => { + resolveCalledPromise(resolveCompletePromise); + }); + return completePromise; }); }); } beforeEach(async () => { - httpBackend = new MockHttpBackend(); - await RustSdkCryptoJs.initAsync(); - const dummyEventEmitter = new TypedEventEmitter(); - const httpApi = new MatrixHttpApi(dummyEventEmitter, { - baseUrl: "https://example.com", - prefix: "/_matrix", - fetchFn: httpBackend.fetchFn as typeof global.fetch, - onlyData: true, - }); - // for these tests we use a mock OlmMachine, with an implementation of outgoingRequests that // returns objects from outgoingRequestQueue outgoingRequestQueue = []; @@ -145,91 +134,55 @@ describe("RustCrypto", () => { 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); - }); + outgoingRequestProcessor = { + makeOutgoingRequest: jest.fn(), + } as unknown as Mocked; - it("should poll for outgoing messages", () => { - rustCrypto.onSyncCompleted({}); - expect(olmMachine.outgoingRequests).toHaveBeenCalled(); + rustCrypto = new RustCrypto(olmMachine, {} as MatrixHttpApi, TEST_USER, TEST_DEVICE_ID); + rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor; }); - /* 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]); + it("should poll for outgoing messages and send them", async () => { + const testReq = new KeysQueryRequest("1234", "{}"); + outgoingRequestQueue.push([testReq]); + const makeRequestPromise = awaitCallToMakeOutgoingRequest(); rustCrypto.onSyncCompleted({}); - await awaitCallToMarkAsSent(); - expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, ""); + await makeRequestPromise; + expect(olmMachine.outgoingRequests).toHaveBeenCalled(); + expect(outgoingRequestProcessor.makeOutgoingRequest).toHaveBeenCalledWith(testReq); }); 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); } + let makeRequestPromise = awaitCallToMakeOutgoingRequest(); + rustCrypto.onSyncCompleted({}); expect(rustCrypto["outgoingRequestLoopRunning"]).toBeTruthy(); // go a couple of times round the loop - await httpBackend.flush("/_matrix", 1); - await awaitCallToMarkAsSent(); + let resolveMakeRequest = await makeRequestPromise; + makeRequestPromise = awaitCallToMakeOutgoingRequest(); + resolveMakeRequest(); - await httpBackend.flush("/_matrix", 1); - await awaitCallToMarkAsSent(); + resolveMakeRequest = await makeRequestPromise; + makeRequestPromise = awaitCallToMakeOutgoingRequest(); + resolveMakeRequest(); // a second sync while this is going on shouldn't make any difference rustCrypto.onSyncCompleted({}); - await httpBackend.flush("/_matrix", 1); - await awaitCallToMarkAsSent(); + resolveMakeRequest = await makeRequestPromise; + outgoingRequestProcessor.makeOutgoingRequest.mockReset(); + resolveMakeRequest(); // now stop... rustCrypto.stop(); @@ -241,7 +194,7 @@ describe("RustCrypto", () => { setTimeout(resolve, 100); }); expect(rustCrypto["outgoingRequestLoopRunning"]).toBeFalsy(); - httpBackend.verifyNoOutstandingRequests(); + expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled(); expect(olmMachine.outgoingRequests).not.toHaveBeenCalled(); // we sent three, so there should be 2 left diff --git a/src/rust-crypto/OutgoingRequestProcessor.ts b/src/rust-crypto/OutgoingRequestProcessor.ts new file mode 100644 index 00000000000..c0163f8525c --- /dev/null +++ b/src/rust-crypto/OutgoingRequestProcessor.ts @@ -0,0 +1,109 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { + OlmMachine, + KeysBackupRequest, + KeysClaimRequest, + KeysQueryRequest, + KeysUploadRequest, + RoomMessageRequest, + SignatureUploadRequest, + ToDeviceRequest, +} from "@matrix-org/matrix-sdk-crypto-js"; + +import { logger } from "../logger"; +import { IHttpOpts, MatrixHttpApi, Method } from "../http-api"; +import { QueryDict } from "../utils"; + +/** + * Common interface for all the request types returned by `OlmMachine.outgoingRequests`. + */ +export interface OutgoingRequest { + readonly id: string | undefined; + readonly type: number; +} + +/** + * OutgoingRequestManager: turns `OutgoingRequest`s from the rust sdk into HTTP requests + * + * We have one of these per `RustCrypto` (and hence per `MatrixClient`), not that it does anything terribly complicated. + * It's responsible for: + * + * * holding the reference to the `MatrixHttpApi` + * * turning `OutgoingRequest`s from the rust backend into HTTP requests, and sending them + * * sending the results of such requests back to the rust backend. + */ +export class OutgoingRequestProcessor { + public constructor( + private readonly olmMachine: OlmMachine, + private readonly http: MatrixHttpApi, + ) {} + + public async makeOutgoingRequest(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 if (msg instanceof ToDeviceRequest) { + const path = + `/_matrix/client/v3/sendToDevice/${encodeURIComponent(msg.event_type)}/` + + encodeURIComponent(msg.txn_id); + resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body); + } else if (msg instanceof RoomMessageRequest) { + const path = + `/_matrix/client/v3/room/${encodeURIComponent(msg.room_id)}/send/` + + `${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`; + resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body); + } else { + logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg)); + resp = ""; + } + + if (msg.id) { + await this.olmMachine.markRequestAsSent(msg.id, msg.type, resp); + } + } + + private async rawJsonRequest(method: Method, path: string, queryParams: QueryDict, body: string): Promise { + const opts = { + // inhibit the JSON stringification and parsing within HttpApi. + json: false, + + // nevertheless, we are sending, and accept, JSON. + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + }, + + // we use the full prefix + prefix: "", + }; + + return await this.http.authedRequest(method, path, queryParams, body, opts); + } +} diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 667f4d3229a..fd553fc94a5 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -15,14 +15,6 @@ limitations under the License. */ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; -import { - DecryptedRoomEvent, - KeysBackupRequest, - KeysClaimRequest, - KeysQueryRequest, - KeysUploadRequest, - SignatureUploadRequest, -} from "@matrix-org/matrix-sdk-crypto-js"; import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto"; import type { IToDeviceEvent } from "../sync-accumulator"; @@ -30,17 +22,9 @@ import type { IEncryptedEventInfo } from "../crypto/api"; import { MatrixEvent } from "../models/event"; import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; import { logger } from "../logger"; -import { IHttpOpts, MatrixHttpApi, Method } from "../http-api"; -import { QueryDict } from "../utils"; +import { IHttpOpts, MatrixHttpApi } from "../http-api"; import { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning"; - -/** - * Common interface for all the request types returned by `OlmMachine.outgoingRequests`. - */ -interface OutgoingRequest { - readonly id: string | undefined; - readonly type: number; -} +import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; /** * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. @@ -55,12 +39,16 @@ export class RustCrypto implements CryptoBackend { /** whether {@link outgoingRequestLoop} is currently running */ private outgoingRequestLoopRunning = false; + private outgoingRequestProcessor: OutgoingRequestProcessor; + public constructor( private readonly olmMachine: RustSdkCryptoJs.OlmMachine, - private readonly http: MatrixHttpApi, + http: MatrixHttpApi, _userId: string, _deviceId: string, - ) {} + ) { + this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http); + } public stop(): void { // stop() may be called multiple times, but attempting to close() the OlmMachine twice @@ -87,7 +75,7 @@ export class RustCrypto implements CryptoBackend { origin_server_ts: event.getTs(), }), new RustSdkCryptoJs.RoomId(event.getRoomId()!), - )) as DecryptedRoomEvent; + )) as RustSdkCryptoJs.DecryptedRoomEvent; return { clearEvent: JSON.parse(res.event), claimedEd25519Key: res.senderClaimedEd25519Key, @@ -190,7 +178,7 @@ export class RustCrypto implements CryptoBackend { return; } for (const msg of outgoingRequests) { - await this.doOutgoingRequest(msg as OutgoingRequest); + await this.outgoingRequestProcessor.makeOutgoingRequest(msg as OutgoingRequest); } } } catch (e) { @@ -199,49 +187,4 @@ export class RustCrypto implements CryptoBackend { this.outgoingRequestLoopRunning = false; } } - - private async doOutgoingRequest(msg: OutgoingRequest): Promise { - let resp: string; - - /* refer https://docs.rs/matrix-sdk-crypto/0.6.0/matrix_sdk_crypto/requests/enum.OutgoingRequests.html - * for the complete list of request types - */ - if (msg instanceof KeysUploadRequest) { - resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body); - } else if (msg instanceof KeysQueryRequest) { - resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body); - } else if (msg instanceof KeysClaimRequest) { - resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body); - } else if (msg instanceof SignatureUploadRequest) { - resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body); - } else if (msg instanceof KeysBackupRequest) { - resp = await this.rawJsonRequest(Method.Put, "/_matrix/client/v3/room_keys/keys", {}, msg.body); - } else { - // TODO: ToDeviceRequest, RoomMessageRequest - logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg)); - resp = ""; - } - - if (msg.id) { - await this.olmMachine.markRequestAsSent(msg.id, msg.type, resp); - } - } - - private async rawJsonRequest(method: Method, path: string, queryParams: QueryDict, body: string): Promise { - const opts = { - // inhibit the JSON stringification and parsing within HttpApi. - json: false, - - // nevertheless, we are sending, and accept, JSON. - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - }, - - // we use the full prefix - prefix: "", - }; - - return await this.http.authedRequest(method, path, queryParams, body, opts); - } } diff --git a/yarn.lock b/yarn.lock index 924097c143f..d9cc1ac4a37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1398,7 +1398,7 @@ dependencies: lodash "^4.17.21" -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.2": +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.3": version "0.1.0-alpha.3" resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.3.tgz#829ea03bcad8051dc1e4f0da18a66d4ba273f78f" integrity sha512-KpEddjC34aobFlUYf2mIaXqkjLC0goRmYnbDZLTd0MwiFtau4b1TrPptQ8XFc90Z2VeAcvf18CBqA2otmZzUKQ== From 6c6304a6203cbf46163dfc2b748fa10fd2194bc2 Mon Sep 17 00:00:00 2001 From: Germain Date: Tue, 31 Jan 2023 16:59:13 +0000 Subject: [PATCH 29/45] Cleanup pre MSC3773 thread unread notif logic (#3115) --- spec/unit/read-receipt.spec.ts | 52 ---------------------------------- src/client.ts | 8 +----- src/models/room.ts | 7 ++--- 3 files changed, 3 insertions(+), 64 deletions(-) diff --git a/spec/unit/read-receipt.spec.ts b/spec/unit/read-receipt.spec.ts index 46e46b1b356..2bc941ab0af 100644 --- a/spec/unit/read-receipt.spec.ts +++ b/spec/unit/read-receipt.spec.ts @@ -18,7 +18,6 @@ import MockHttpBackend from "matrix-mock-request"; import { MAIN_ROOM_TIMELINE, ReceiptType } from "../../src/@types/read_receipts"; import { MatrixClient } from "../../src/client"; -import { Feature, ServerSupport } from "../../src/feature"; import { EventType } from "../../src/matrix"; import { synthesizeReceipt } from "../../src/models/read-receipt"; import { encodeUri } from "../../src/utils"; @@ -70,10 +69,6 @@ const roomEvent = utils.mkEvent({ }, }); -function mockServerSideSupport(client: MatrixClient, serverSideSupport: ServerSupport) { - client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport); -} - describe("Read receipt", () => { beforeEach(() => { httpBackend = new MockHttpBackend(); @@ -101,7 +96,6 @@ describe("Read receipt", () => { }) .respond(200, {}); - mockServerSideSupport(client, ServerSupport.Stable); client.sendReceipt(threadEvent, ReceiptType.Read, {}); await httpBackend.flushAllExpected(); @@ -123,7 +117,6 @@ describe("Read receipt", () => { }) .respond(200, {}); - mockServerSideSupport(client, ServerSupport.Stable); client.sendReadReceipt(threadEvent, ReceiptType.Read, true); await httpBackend.flushAllExpected(); @@ -145,56 +138,11 @@ describe("Read receipt", () => { }) .respond(200, {}); - mockServerSideSupport(client, ServerSupport.Stable); client.sendReceipt(roomEvent, ReceiptType.Read, {}); await httpBackend.flushAllExpected(); await flushPromises(); }); - - it("sends a room read receipt when there's no server support", async () => { - httpBackend - .when( - "POST", - encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", { - $roomId: ROOM_ID, - $receiptType: ReceiptType.Read, - $eventId: threadEvent.getId()!, - }), - ) - .check((request) => { - expect(request.data.thread_id).toBeUndefined(); - }) - .respond(200, {}); - - mockServerSideSupport(client, ServerSupport.Unsupported); - client.sendReceipt(threadEvent, ReceiptType.Read, {}); - - await httpBackend.flushAllExpected(); - await flushPromises(); - }); - - it("sends a valid room read receipt even when body omitted", async () => { - httpBackend - .when( - "POST", - encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", { - $roomId: ROOM_ID, - $receiptType: ReceiptType.Read, - $eventId: threadEvent.getId()!, - }), - ) - .check((request) => { - expect(request.data).toEqual({}); - }) - .respond(200, {}); - - mockServerSideSupport(client, ServerSupport.Unsupported); - client.sendReceipt(threadEvent, ReceiptType.Read, undefined); - - await httpBackend.flushAllExpected(); - await flushPromises(); - }); }); describe("synthesizeReceipt", () => { diff --git a/src/client.ts b/src/client.ts index 22a0a1d068f..ad00c95a909 100644 --- a/src/client.ts +++ b/src/client.ts @@ -209,7 +209,6 @@ import { ToDeviceBatch } from "./models/ToDeviceMessage"; import { IgnoredInvites } from "./models/invites-ignorer"; import { UIARequest, UIAResponse } from "./@types/uia"; 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"; @@ -4840,8 +4839,7 @@ export class MatrixClient extends TypedEventEmitter { */ public getUnreadNotificationCount(type = NotificationCountType.Total): number { let count = this.getRoomUnreadNotificationCount(type); - if (this.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) { - for (const threadNotification of this.threadNotifications.values()) { - count += threadNotification[type] ?? 0; - } + for (const threadNotification of this.threadNotifications.values()) { + count += threadNotification[type] ?? 0; } return count; } From fbd2c97f87c845f4d8898ffce77a20a83eaa40e1 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 1 Feb 2023 10:44:32 +0000 Subject: [PATCH 30/45] Move the logic of roomPredecessor into the RoomState class (#3114) * Move the logic of roomPredecessor into the RoomState class * Fix review comments --- src/models/room-state.ts | 45 ++++++++++++++++++++++++++++++++++++++++ src/models/room.ts | 25 +++------------------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/src/models/room-state.ts b/src/models/room-state.ts index a17d92b568d..77693e9743e 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -964,6 +964,51 @@ export class RoomState extends TypedEventEmitter return guestAccessContent["guest_access"] || GuestAccess.Forbidden; } + /** + * Find the predecessor room based on this room state. + * + * @param msc3946ProcessDynamicPredecessor - if true, look for an + * m.room.predecessor state event and use it if found (MSC3946). + * @returns null if this room has no predecessor. Otherwise, returns + * the roomId and last eventId of the predecessor room. + * If msc3946ProcessDynamicPredecessor is true, use m.predecessor events + * as well as m.room.create events to find predecessors. + * Note: if an m.predecessor event is used, eventId is null since those + * events do not include an event_id property. + */ + public findPredecessor( + msc3946ProcessDynamicPredecessor = false, + ): { roomId: string; eventId: string | null } | null { + // Note: the tests for this function are against Room.findPredecessor, + // which just calls through to here. + + if (msc3946ProcessDynamicPredecessor) { + const predecessorEvent = this.getStateEvents(EventType.RoomPredecessor, ""); + if (predecessorEvent) { + const roomId = predecessorEvent.getContent()["predecessor_room_id"]; + if (typeof roomId === "string") { + return { roomId, eventId: null }; + } + } + } + + const createEvent = this.getStateEvents(EventType.RoomCreate, ""); + if (createEvent) { + const predecessor = createEvent.getContent()["predecessor"]; + if (predecessor) { + const roomId = predecessor["room_id"]; + if (typeof roomId === "string") { + let eventId = predecessor["event_id"]; + if (typeof eventId !== "string" || eventId === "") { + eventId = null; + } + return { roomId, eventId }; + } + } + } + return null; + } + private updateThirdPartyTokenCache(memberEvent: MatrixEvent): void { if (!memberEvent.getContent().third_party_invite) { return; diff --git a/src/models/room.ts b/src/models/room.ts index 496dd6e8d2b..a82efcc6f85 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -3025,6 +3025,8 @@ export class Room extends ReadReceipt { } /** + * Find the predecessor of this room. + * * @param msc3946ProcessDynamicPredecessor - if true, look for an * m.room.predecessor state event and use it if found (MSC3946). * @returns null if this room has no predecessor. Otherwise, returns @@ -3041,28 +3043,7 @@ export class Room extends ReadReceipt { if (!currentState) { return null; } - if (msc3946ProcessDynamicPredecessor) { - const predecessorEvent = currentState.getStateEvents(EventType.RoomPredecessor, ""); - if (predecessorEvent) { - const roomId = predecessorEvent.getContent()["predecessor_room_id"]; - if (roomId) { - return { roomId, eventId: null }; - } - } - } - - const createEvent = currentState.getStateEvents(EventType.RoomCreate, ""); - if (createEvent) { - const predecessor = createEvent.getContent()["predecessor"]; - if (predecessor) { - const roomId = predecessor["room_id"]; - if (roomId) { - const eventId = predecessor["event_id"] || null; - return { roomId, eventId }; - } - } - } - return null; + return currentState.findPredecessor(msc3946ProcessDynamicPredecessor); } private roomNameGenerator(state: RoomNameState): string { From b2a9e6f12f86bc690daa8ed632e277a0835a643f Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Wed, 1 Feb 2023 14:31:07 +0000 Subject: [PATCH 31/45] Handle optional last_known_event_id property in m.predecessor (#3119) --- spec/unit/room.spec.ts | 32 ++++++++++++++++++++++++++------ src/models/room-state.ts | 6 +++++- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 83ae123ffdb..4fe46ee8dae 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -3340,11 +3340,18 @@ describe("Room", function () { }); } - function predecessorEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent { + function predecessorEvent( + newRoomId: string, + predecessorRoomId: string, + tombstoneEventId: string | null = null, + ): MatrixEvent { + const content = + tombstoneEventId === null + ? { predecessor_room_id: predecessorRoomId } + : { predecessor_room_id: predecessorRoomId, last_known_event_id: tombstoneEventId }; + return new MatrixEvent({ - content: { - predecessor_room_id: predecessorRoomId, - }, + content, event_id: `predecessor_event_id_pred_${predecessorRoomId}`, origin_server_ts: 1432735824653, room_id: newRoomId, @@ -3380,7 +3387,20 @@ describe("Room", function () { const useMsc3946 = true; expect(room.findPredecessor(useMsc3946)).toEqual({ roomId: "otherreplacedroomid", - eventId: null, // m.predecessor does not include an event_id + eventId: null, // m.predecessor did not include an event_id + }); + }); + + it("uses the m.predecessor event ID if provided", () => { + const room = new Room("roomid", client!, "@u:example.com"); + room.addLiveEvents([ + roomCreateEvent("roomid", "replacedroomid"), + predecessorEvent("roomid", "otherreplacedroomid", "lstevtid"), + ]); + const useMsc3946 = true; + expect(room.findPredecessor(useMsc3946)).toEqual({ + roomId: "otherreplacedroomid", + eventId: "lstevtid", }); }); @@ -3399,7 +3419,7 @@ describe("Room", function () { const room = new Room("roomid", client!, "@u:example.com"); room.addLiveEvents([ roomCreateEvent("roomid", null), // Create event has no predecessor - predecessorEvent("roomid", "otherreplacedroomid"), + predecessorEvent("roomid", "otherreplacedroomid", "lastevtid"), ]); // Don't provide an argument for msc3946ProcessDynamicPredecessor - // we should ignore the predecessor event. diff --git a/src/models/room-state.ts b/src/models/room-state.ts index 77693e9743e..e704d27a8cf 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -986,8 +986,12 @@ export class RoomState extends TypedEventEmitter const predecessorEvent = this.getStateEvents(EventType.RoomPredecessor, ""); if (predecessorEvent) { const roomId = predecessorEvent.getContent()["predecessor_room_id"]; + let eventId = predecessorEvent.getContent()["last_known_event_id"]; + if (typeof eventId !== "string") { + eventId = null; + } if (typeof roomId === "string") { - return { roomId, eventId: null }; + return { roomId, eventId }; } } } From 2800681bb1f750476ea9d388bffaa7c8585b5078 Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 2 Feb 2023 09:32:37 +1300 Subject: [PATCH 32/45] Poll model - validate end events (#3072) * first cut poll model * process incoming poll relations * allow alt event types in relations model * allow alt event types in relations model * remove unneccesary checks on remove relation * comment * Revert "allow alt event types in relations model" This reverts commit e578d84464403d4a15ee8a7cf3ac643f4fb86d69. * Revert "Revert "allow alt event types in relations model"" This reverts commit 515db7a8bc2df5a1c619a37c86e17ccbe287ba7a. * basic handling for new poll relations * tests * test room.processPollEvents * join processBeaconEvents and poll events in client * tidy and set 23 copyrights * use rooms instance of matrixClient * tidy * more copyright * simplify processPollEvent code * throw when poll start event has no roomId * updates for events-sdk move * more type changes for events-sdk changes * validate poll end event senders * reformatted copyright * undo more comment reformatting * fix poll end validation logic to allow poll creator to end poll regardless of redaction * Update src/models/poll.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * correct creator == sender validationin poll end --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- spec/test-utils/client.ts | 1 + spec/unit/models/poll.spec.ts | 165 +++++++++++++++++++++++++++++----- src/models/poll.ts | 35 +++++++- src/models/room.ts | 2 +- 4 files changed, 177 insertions(+), 26 deletions(-) diff --git a/spec/test-utils/client.ts b/spec/test-utils/client.ts index 800730fd528..1e8a4643893 100644 --- a/spec/test-utils/client.ts +++ b/spec/test-utils/client.ts @@ -60,6 +60,7 @@ export const getMockClientWithEventEmitter = ( */ export const mockClientMethodsUser = (userId = "@alice:domain") => ({ getUserId: jest.fn().mockReturnValue(userId), + getSafeUserId: jest.fn().mockReturnValue(userId), getUser: jest.fn().mockReturnValue(new User(userId)), isGuest: jest.fn().mockReturnValue(false), mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"), diff --git a/spec/unit/models/poll.spec.ts b/spec/unit/models/poll.spec.ts index feb0c27ffab..c6bc39d53ac 100644 --- a/spec/unit/models/poll.spec.ts +++ b/spec/unit/models/poll.spec.ts @@ -14,26 +14,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IEvent, MatrixEvent, PollEvent } from "../../../src"; +import { IEvent, MatrixEvent, PollEvent, Room } from "../../../src"; import { REFERENCE_RELATION } from "../../../src/@types/extensible_events"; import { M_POLL_END, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE } from "../../../src/@types/polls"; import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent"; import { Poll } from "../../../src/models/poll"; -import { getMockClientWithEventEmitter } from "../../test-utils/client"; +import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client"; jest.useFakeTimers(); describe("Poll", () => { + const userId = "@alice:server.org"; const mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), relations: jest.fn(), }); const roomId = "!room:server"; + const room = new Room(roomId, mockClient, userId); + const maySendRedactionForEventSpy = jest.spyOn(room.currentState, "maySendRedactionForEvent"); // 14.03.2022 16:15 const now = 1647270879403; const basePollStartEvent = new MatrixEvent({ ...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(), room_id: roomId, + sender: userId, }); basePollStartEvent.event.event_id = "$12345"; @@ -42,6 +47,8 @@ describe("Poll", () => { jest.setSystemTime(now); mockClient.relations.mockResolvedValue({ events: [] }); + + maySendRedactionForEventSpy.mockClear().mockReturnValue(true); }); let eventId = 1; @@ -62,7 +69,7 @@ describe("Poll", () => { }; it("initialises with root event", () => { - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); expect(poll.roomId).toEqual(roomId); expect(poll.pollId).toEqual(basePollStartEvent.getId()); expect(poll.pollEvent).toEqual(basePollStartEvent.unstableExtensibleEvent); @@ -73,7 +80,7 @@ describe("Poll", () => { const pollStartEvent = new MatrixEvent( PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(), ); - expect(() => new Poll(pollStartEvent, mockClient)).toThrowError("Invalid poll start event."); + expect(() => new Poll(pollStartEvent, mockClient, room)).toThrowError("Invalid poll start event."); }); it("throws when poll start has no event id", () => { @@ -81,12 +88,12 @@ describe("Poll", () => { ...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(), room_id: roomId, }); - expect(() => new Poll(pollStartEvent, mockClient)).toThrowError("Invalid poll start event."); + expect(() => new Poll(pollStartEvent, mockClient, room)).toThrowError("Invalid poll start event."); }); describe("fetching responses", () => { it("calls relations api and emits", async () => { - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); const emitSpy = jest.spyOn(poll, "emit"); const responses = await poll.getResponses(); expect(mockClient.relations).toHaveBeenCalledWith(roomId, basePollStartEvent.getId(), "m.reference"); @@ -94,7 +101,7 @@ describe("Poll", () => { }); it("returns existing responses object after initial fetch", async () => { - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); const responses = await poll.getResponses(); const responses2 = await poll.getResponses(); // only fetched relations once @@ -104,7 +111,7 @@ describe("Poll", () => { }); it("waits for existing relations request to finish when getting responses", async () => { - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); const firstResponsePromise = poll.getResponses(); const secondResponsePromise = poll.getResponses(); await firstResponsePromise; @@ -121,14 +128,14 @@ describe("Poll", () => { mockClient.relations.mockResolvedValue({ events: [replyEvent, stableResponseEvent, unstableResponseEvent], }); - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); const responses = await poll.getResponses(); expect(responses.getRelations()).toEqual([stableResponseEvent, unstableResponseEvent]); }); describe("with poll end event", () => { - const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable! }); - const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable! }); + const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob@server.org" }); + const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable!, sender: "@bob@server.org" }); const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000); const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now); const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000); @@ -140,10 +147,43 @@ describe("Poll", () => { }); it("sets poll end event with stable event type", async () => { - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); + jest.spyOn(poll, "emit"); + await poll.getResponses(); + + expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org"); + expect(poll.isEnded).toBe(true); + expect(poll.emit).toHaveBeenCalledWith(PollEvent.End); + }); + + it("does not set poll end event when sent by a user without redaction rights", async () => { + const poll = new Poll(basePollStartEvent, mockClient, room); + maySendRedactionForEventSpy.mockReturnValue(false); + jest.spyOn(poll, "emit"); + await poll.getResponses(); + + expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org"); + expect(poll.isEnded).toBe(false); + expect(poll.emit).not.toHaveBeenCalledWith(PollEvent.End); + }); + + it("sets poll end event when endevent sender also created the poll, but does not have redaction rights", async () => { + const pollStartEvent = new MatrixEvent({ + ...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(), + room_id: roomId, + sender: "@bob:domain.org", + }); + pollStartEvent.event.event_id = "$6789"; + const poll = new Poll(pollStartEvent, mockClient, room); + const pollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob:domain.org" }); + mockClient.relations.mockResolvedValue({ + events: [pollEndEvent], + }); + maySendRedactionForEventSpy.mockReturnValue(false); jest.spyOn(poll, "emit"); await poll.getResponses(); + expect(maySendRedactionForEventSpy).not.toHaveBeenCalled(); expect(poll.isEnded).toBe(true); expect(poll.emit).toHaveBeenCalledWith(PollEvent.End); }); @@ -152,7 +192,7 @@ describe("Poll", () => { mockClient.relations.mockResolvedValue({ events: [unstablePollEndEvent], }); - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); jest.spyOn(poll, "emit"); await poll.getResponses(); @@ -161,7 +201,7 @@ describe("Poll", () => { }); it("filters out responses that were sent after poll end", async () => { - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); const responses = await poll.getResponses(); // just response type events @@ -173,7 +213,7 @@ describe("Poll", () => { describe("onNewRelation()", () => { it("discards response if poll responses have not been initialised", () => { - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); jest.spyOn(poll, "emit"); const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now); @@ -184,24 +224,107 @@ describe("Poll", () => { }); it("sets poll end event when responses are not initialised", () => { - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); jest.spyOn(poll, "emit"); - const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable! }); + const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId }); poll.onNewRelation(stablePollEndEvent); expect(poll.emit).toHaveBeenCalledWith(PollEvent.End); }); + it("does not set poll end event when sent by invalid user", async () => { + maySendRedactionForEventSpy.mockReturnValue(false); + const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@charlie:server.org" }); + const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000); + mockClient.relations.mockResolvedValue({ + events: [responseEventAfterEnd], + }); + const poll = new Poll(basePollStartEvent, mockClient, room); + await poll.getResponses(); + jest.spyOn(poll, "emit"); + + poll.onNewRelation(stablePollEndEvent); + + // didn't end, didn't refilter responses + expect(poll.emit).not.toHaveBeenCalled(); + expect(poll.isEnded).toBeFalsy(); + expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@charlie:server.org"); + }); + + it("does not set poll end event when an earlier end event already exists", async () => { + const earlierPollEndEvent = makeRelatedEvent( + { type: M_POLL_END.stable!, sender: "@valid:server.org" }, + now, + ); + const laterPollEndEvent = makeRelatedEvent( + { type: M_POLL_END.stable!, sender: "@valid:server.org" }, + now + 2000, + ); + + const poll = new Poll(basePollStartEvent, mockClient, room); + await poll.getResponses(); + + poll.onNewRelation(earlierPollEndEvent); + + // first end event set correctly + expect(poll.isEnded).toBeTruthy(); + + // reset spy count + jest.spyOn(poll, "emit").mockClear(); + + poll.onNewRelation(laterPollEndEvent); + // didn't set new end event, didn't refilter responses + expect(poll.emit).not.toHaveBeenCalled(); + expect(poll.isEnded).toBeTruthy(); + }); + + it("replaces poll end event and refilters when an older end event already exists", async () => { + const earlierPollEndEvent = makeRelatedEvent( + { type: M_POLL_END.stable!, sender: "@valid:server.org" }, + now, + ); + const laterPollEndEvent = makeRelatedEvent( + { type: M_POLL_END.stable!, sender: "@valid:server.org" }, + now + 2000, + ); + const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000); + const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now); + const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000); + mockClient.relations.mockResolvedValue({ + events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, laterPollEndEvent], + }); + + const poll = new Poll(basePollStartEvent, mockClient, room); + const responses = await poll.getResponses(); + + // all responses have a timestamp < laterPollEndEvent + expect(responses.getRelations().length).toEqual(3); + // first end event set correctly + expect(poll.isEnded).toBeTruthy(); + + // reset spy count + jest.spyOn(poll, "emit").mockClear(); + + // add a valid end event with earlier timestamp + poll.onNewRelation(earlierPollEndEvent); + + // emitted new end event + expect(poll.emit).toHaveBeenCalledWith(PollEvent.End); + // filtered responses and emitted + expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses); + expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]); + }); + it("sets poll end event and refilters responses based on timestamp", async () => { - const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable! }); + const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId }); const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000); const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now); const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000); mockClient.relations.mockResolvedValue({ events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd], }); - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); const responses = await poll.getResponses(); jest.spyOn(poll, "emit"); @@ -216,7 +339,7 @@ describe("Poll", () => { }); it("filters out irrelevant relations", async () => { - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); // init responses const responses = await poll.getResponses(); jest.spyOn(poll, "emit"); @@ -230,7 +353,7 @@ describe("Poll", () => { }); it("adds poll response relations to responses", async () => { - const poll = new Poll(basePollStartEvent, mockClient); + const poll = new Poll(basePollStartEvent, mockClient, room); // init responses const responses = await poll.getResponses(); jest.spyOn(poll, "emit"); diff --git a/src/models/poll.ts b/src/models/poll.ts index 612b6a0991f..c51c39168f9 100644 --- a/src/models/poll.ts +++ b/src/models/poll.ts @@ -18,6 +18,7 @@ import { M_POLL_END, M_POLL_RESPONSE, PollStartEvent } from "../@types/polls"; import { MatrixClient } from "../client"; import { MatrixEvent } from "./event"; import { Relations } from "./relations"; +import { Room } from "./room"; import { TypedEventEmitter } from "./typed-event-emitter"; export enum PollEvent { @@ -64,13 +65,12 @@ export class Poll extends TypedEventEmitter, P private responses: null | Relations = null; private endEvent: MatrixEvent | undefined; - public constructor(private rootEvent: MatrixEvent, private matrixClient: MatrixClient) { + public constructor(private rootEvent: MatrixEvent, private matrixClient: MatrixClient, private room: Room) { super(); if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) { throw new Error("Invalid poll start event."); } this.roomId = this.rootEvent.getRoomId()!; - // @TODO(kerrya) proper way to do this? this.pollEvent = this.rootEvent.unstableExtensibleEvent as unknown as PollStartEvent; } @@ -101,7 +101,7 @@ export class Poll extends TypedEventEmitter, P * @returns void */ public onNewRelation(event: MatrixEvent): void { - if (M_POLL_END.matches(event.getType())) { + if (M_POLL_END.matches(event.getType()) && this.validateEndEvent(event)) { this.endEvent = event; this.refilterResponsesOnEnd(); this.emit(PollEvent.End); @@ -136,7 +136,8 @@ export class Poll extends TypedEventEmitter, P M_POLL_RESPONSE.altName!, ]); - const pollEndEvent = allRelations.events.find((event) => M_POLL_END.matches(event.getType())); + const potentialEndEvent = allRelations.events.find((event) => M_POLL_END.matches(event.getType())); + const pollEndEvent = this.validateEndEvent(potentialEndEvent) ? potentialEndEvent : undefined; const pollCloseTimestamp = pollEndEvent?.getTs() || Number.MAX_SAFE_INTEGER; const { responseEvents } = filterResponseRelations(allRelations.events, pollCloseTimestamp); @@ -172,4 +173,30 @@ export class Poll extends TypedEventEmitter, P this.emit(PollEvent.Responses, this.responses); } + + private validateEndEvent(endEvent?: MatrixEvent): boolean { + if (!endEvent) { + return false; + } + /** + * Repeated end events are ignored - + * only the first (valid) closure event by origin_server_ts is counted. + */ + if (this.endEvent && this.endEvent.getTs() < endEvent.getTs()) { + return false; + } + + /** + * MSC3381 + * If a m.poll.end event is received from someone other than the poll creator or user with permission to redact + * others' messages in the room, the event must be ignored by clients due to being invalid. + */ + const roomCurrentState = this.room.currentState; + const endEventSender = endEvent.getSender(); + return ( + !!endEventSender && + (endEventSender === this.rootEvent.getSender() || + roomCurrentState.maySendRedactionForEvent(this.rootEvent, endEventSender)) + ); + } } diff --git a/src/models/room.ts b/src/models/room.ts index a82efcc6f85..8d478789b9d 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1897,7 +1897,7 @@ export class Room extends ReadReceipt { const processPollStartEvent = (event: MatrixEvent): void => { if (!M_POLL_START.matches(event.getType())) return; try { - const poll = new Poll(event, this.client); + const poll = new Poll(event, this.client, this); this.polls.set(event.getId()!, poll); this.emit(PollEvent.New, poll); } catch {} From 4e8affafcc9198b5377d6b5fb87d692e095979c3 Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 2 Feb 2023 09:44:40 +1300 Subject: [PATCH 33/45] Poll model - page /relations results (#3073) * first cut poll model * process incoming poll relations * allow alt event types in relations model * allow alt event types in relations model * remove unneccesary checks on remove relation * comment * Revert "allow alt event types in relations model" This reverts commit e578d84464403d4a15ee8a7cf3ac643f4fb86d69. * Revert "Revert "allow alt event types in relations model"" This reverts commit 515db7a8bc2df5a1c619a37c86e17ccbe287ba7a. * basic handling for new poll relations * tests * test room.processPollEvents * join processBeaconEvents and poll events in client * tidy and set 23 copyrights * use rooms instance of matrixClient * tidy * more copyright * simplify processPollEvent code * throw when poll start event has no roomId * updates for events-sdk move * more type changes for events-sdk changes * page poll relation results * validate poll end event senders * reformatted copyright * undo more comment reformatting * test paging * use correct pollstartevent type * emit after updating _isFetchingResponses state * make rootEvent public readonly * fix poll end validation logic to allow poll creator to end poll regardless of redaction --- spec/unit/models/poll.spec.ts | 151 ++++++++++++++++++++++++++++++---- src/models/poll.ts | 68 +++++++++++---- 2 files changed, 187 insertions(+), 32 deletions(-) diff --git a/spec/unit/models/poll.spec.ts b/spec/unit/models/poll.spec.ts index c6bc39d53ac..6b00e4a5dbd 100644 --- a/spec/unit/models/poll.spec.ts +++ b/spec/unit/models/poll.spec.ts @@ -46,7 +46,7 @@ describe("Poll", () => { jest.clearAllMocks(); jest.setSystemTime(now); - mockClient.relations.mockResolvedValue({ events: [] }); + mockClient.relations.mockReset().mockResolvedValue({ events: [] }); maySendRedactionForEventSpy.mockClear().mockReturnValue(true); }); @@ -95,8 +95,17 @@ describe("Poll", () => { it("calls relations api and emits", async () => { const poll = new Poll(basePollStartEvent, mockClient, room); const emitSpy = jest.spyOn(poll, "emit"); - const responses = await poll.getResponses(); - expect(mockClient.relations).toHaveBeenCalledWith(roomId, basePollStartEvent.getId(), "m.reference"); + const fetchResponsePromise = poll.getResponses(); + expect(poll.isFetchingResponses).toBe(true); + const responses = await fetchResponsePromise; + expect(poll.isFetchingResponses).toBe(false); + expect(mockClient.relations).toHaveBeenCalledWith( + roomId, + basePollStartEvent.getId(), + "m.reference", + undefined, + { from: undefined }, + ); expect(emitSpy).toHaveBeenCalledWith(PollEvent.Responses, responses); }); @@ -133,6 +142,48 @@ describe("Poll", () => { expect(responses.getRelations()).toEqual([stableResponseEvent, unstableResponseEvent]); }); + describe("with multiple pages of relations", () => { + const makeResponses = (count = 1, timestamp = now): MatrixEvent[] => + new Array(count) + .fill("x") + .map((_x, index) => + makeRelatedEvent( + { type: M_POLL_RESPONSE.stable!, sender: "@bob@server.org" }, + timestamp + index, + ), + ); + + it("page relations responses", async () => { + const responseEvents = makeResponses(6); + mockClient.relations + .mockResolvedValueOnce({ + events: responseEvents.slice(0, 2), + nextBatch: "test-next-1", + }) + .mockResolvedValueOnce({ + events: responseEvents.slice(2, 4), + nextBatch: "test-next-2", + }) + .mockResolvedValueOnce({ + events: responseEvents.slice(4), + }); + + const poll = new Poll(basePollStartEvent, mockClient, room); + jest.spyOn(poll, "emit"); + const responses = await poll.getResponses(); + + expect(mockClient.relations.mock.calls).toEqual([ + [roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: undefined }], + [roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-1" }], + [roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-2" }], + ]); + + expect(poll.emit).toHaveBeenCalledTimes(3); + expect(poll.isFetchingResponses).toBeFalsy(); + expect(responses.getRelations().length).toEqual(6); + }); + }); + describe("with poll end event", () => { const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob@server.org" }); const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable!, sender: "@bob@server.org" }); @@ -156,17 +207,6 @@ describe("Poll", () => { expect(poll.emit).toHaveBeenCalledWith(PollEvent.End); }); - it("does not set poll end event when sent by a user without redaction rights", async () => { - const poll = new Poll(basePollStartEvent, mockClient, room); - maySendRedactionForEventSpy.mockReturnValue(false); - jest.spyOn(poll, "emit"); - await poll.getResponses(); - - expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org"); - expect(poll.isEnded).toBe(false); - expect(poll.emit).not.toHaveBeenCalledWith(PollEvent.End); - }); - it("sets poll end event when endevent sender also created the poll, but does not have redaction rights", async () => { const pollStartEvent = new MatrixEvent({ ...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(), @@ -316,6 +356,89 @@ describe("Poll", () => { expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]); }); + it("does not set poll end event when sent by invalid user", async () => { + maySendRedactionForEventSpy.mockReturnValue(false); + const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@charlie:server.org" }); + const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000); + mockClient.relations.mockResolvedValue({ + events: [responseEventAfterEnd], + }); + const poll = new Poll(basePollStartEvent, mockClient, room); + await poll.getResponses(); + jest.spyOn(poll, "emit"); + + poll.onNewRelation(stablePollEndEvent); + + // didn't end, didn't refilter responses + expect(poll.emit).not.toHaveBeenCalled(); + expect(poll.isEnded).toBeFalsy(); + expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@charlie:server.org"); + }); + + it("does not set poll end event when an earlier end event already exists", async () => { + const earlierPollEndEvent = makeRelatedEvent( + { type: M_POLL_END.stable!, sender: "@valid:server.org" }, + now, + ); + const laterPollEndEvent = makeRelatedEvent( + { type: M_POLL_END.stable!, sender: "@valid:server.org" }, + now + 2000, + ); + + const poll = new Poll(basePollStartEvent, mockClient, room); + await poll.getResponses(); + + poll.onNewRelation(earlierPollEndEvent); + + // first end event set correctly + expect(poll.isEnded).toBeTruthy(); + + // reset spy count + jest.spyOn(poll, "emit").mockClear(); + + poll.onNewRelation(laterPollEndEvent); + // didn't set new end event, didn't refilter responses + expect(poll.emit).not.toHaveBeenCalled(); + expect(poll.isEnded).toBeTruthy(); + }); + + it("replaces poll end event and refilters when an older end event already exists", async () => { + const earlierPollEndEvent = makeRelatedEvent( + { type: M_POLL_END.stable!, sender: "@valid:server.org" }, + now, + ); + const laterPollEndEvent = makeRelatedEvent( + { type: M_POLL_END.stable!, sender: "@valid:server.org" }, + now + 2000, + ); + const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000); + const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now); + const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000); + mockClient.relations.mockResolvedValue({ + events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, laterPollEndEvent], + }); + + const poll = new Poll(basePollStartEvent, mockClient, room); + const responses = await poll.getResponses(); + + // all responses have a timestamp < laterPollEndEvent + expect(responses.getRelations().length).toEqual(3); + // first end event set correctly + expect(poll.isEnded).toBeTruthy(); + + // reset spy count + jest.spyOn(poll, "emit").mockClear(); + + // add a valid end event with earlier timestamp + poll.onNewRelation(earlierPollEndEvent); + + // emitted new end event + expect(poll.emit).toHaveBeenCalledWith(PollEvent.End); + // filtered responses and emitted + expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses); + expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]); + }); + it("sets poll end event and refilters responses based on timestamp", async () => { const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId }); const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000); diff --git a/src/models/poll.ts b/src/models/poll.ts index c51c39168f9..7c5d245f7e7 100644 --- a/src/models/poll.ts +++ b/src/models/poll.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { M_POLL_END, M_POLL_RESPONSE, PollStartEvent } from "../@types/polls"; +import { M_POLL_END, M_POLL_RESPONSE } from "../@types/polls"; import { MatrixClient } from "../client"; +import { PollStartEvent } from "../extensible_events_v1/PollStartEvent"; import { MatrixEvent } from "./event"; import { Relations } from "./relations"; import { Room } from "./room"; @@ -61,11 +62,12 @@ const filterResponseRelations = ( export class Poll extends TypedEventEmitter, PollEventHandlerMap> { public readonly roomId: string; public readonly pollEvent: PollStartEvent; - private fetchingResponsesPromise: null | Promise = null; + private _isFetchingResponses = false; + private relationsNextBatch: string | undefined; private responses: null | Relations = null; private endEvent: MatrixEvent | undefined; - public constructor(private rootEvent: MatrixEvent, private matrixClient: MatrixClient, private room: Room) { + public constructor(public readonly rootEvent: MatrixEvent, private matrixClient: MatrixClient, private room: Room) { super(); if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) { throw new Error("Invalid poll start event."); @@ -82,16 +84,23 @@ export class Poll extends TypedEventEmitter, P return !!this.endEvent; } + public get isFetchingResponses(): boolean { + return this._isFetchingResponses; + } + public async getResponses(): Promise { - // if we have already fetched the responses + // if we have already fetched some responses // just return them if (this.responses) { return this.responses; } - if (!this.fetchingResponsesPromise) { - this.fetchingResponsesPromise = this.fetchResponses(); + + // if there is no fetching in progress + // start fetching + if (!this.isFetchingResponses) { + await this.fetchResponses(); } - await this.fetchingResponsesPromise; + // return whatever responses we got from the first page return this.responses!; } @@ -124,21 +133,34 @@ export class Poll extends TypedEventEmitter, P } private async fetchResponses(): Promise { + this._isFetchingResponses = true; + // we want: // - stable and unstable M_POLL_RESPONSE // - stable and unstable M_POLL_END // so make one api call and filter by event type client side - const allRelations = await this.matrixClient.relations(this.roomId, this.rootEvent.getId()!, "m.reference"); + const allRelations = await this.matrixClient.relations( + this.roomId, + this.rootEvent.getId()!, + "m.reference", + undefined, + { + from: this.relationsNextBatch || undefined, + }, + ); - // @TODO(kerrya) paging results + const responses = + this.responses || + new Relations("m.reference", M_POLL_RESPONSE.name, this.matrixClient, [M_POLL_RESPONSE.altName!]); - const responses = new Relations("m.reference", M_POLL_RESPONSE.name, this.matrixClient, [ - M_POLL_RESPONSE.altName!, - ]); + const pollEndEvent = allRelations.events.find((event) => M_POLL_END.matches(event.getType())); + if (this.validateEndEvent(pollEndEvent)) { + this.endEvent = pollEndEvent; + this.refilterResponsesOnEnd(); + this.emit(PollEvent.End); + } - const potentialEndEvent = allRelations.events.find((event) => M_POLL_END.matches(event.getType())); - const pollEndEvent = this.validateEndEvent(potentialEndEvent) ? potentialEndEvent : undefined; - const pollCloseTimestamp = pollEndEvent?.getTs() || Number.MAX_SAFE_INTEGER; + const pollCloseTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER; const { responseEvents } = filterResponseRelations(allRelations.events, pollCloseTimestamp); @@ -146,11 +168,21 @@ export class Poll extends TypedEventEmitter, P responses.addEvent(event); }); + this.relationsNextBatch = allRelations.nextBatch ?? undefined; this.responses = responses; - this.endEvent = pollEndEvent; - if (this.endEvent) { - this.emit(PollEvent.End); + + // while there are more pages of relations + // fetch them + if (this.relationsNextBatch) { + // don't await + // we want to return the first page as soon as possible + this.fetchResponses(); + } else { + // no more pages + this._isFetchingResponses = false; } + + // emit after updating _isFetchingResponses state this.emit(PollEvent.Responses, this.responses); } From 8f5db463e78f536424149cd396efed7f5d9d9090 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 2 Feb 2023 08:28:44 +0000 Subject: [PATCH 34/45] Element R: Fix obscure errors when we fail to decrypt to-device events (#3117) Previously, if we failed to decrypt a to-device event, we would raise an "expected a string" error when we later tried to decrypt it as a room event. This at least makes the error clearer. --- src/rust-crypto/rust-crypto.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index fd553fc94a5..287ab71f415 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -65,6 +65,15 @@ export class RustCrypto implements CryptoBackend { } public async decryptEvent(event: MatrixEvent): Promise { + const roomId = event.getRoomId(); + if (!roomId) { + // presumably, a to-device message. These are normally decrypted in preprocessToDeviceMessages + // so the fact it has come back here suggests that decryption failed. + // + // once we drop support for the libolm crypto implementation, we can stop passing to-device messages + // through decryptEvent and hence get rid of this case. + throw new Error("to-device event was not decrypted in preprocessToDeviceMessages"); + } const res = (await this.olmMachine.decryptRoomEvent( JSON.stringify({ event_id: event.getId(), From 44d2e47f96cc4e9b68f65fcdd8e813dba2587bc7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Feb 2023 10:56:24 +0000 Subject: [PATCH 35/45] Fix invite processing on Element-R (#3121) Currently, whenever we receive an invite on element R, it crashes the sync loop. A quick fix to make it not do that. --- src/sync.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sync.ts b/src/sync.ts index 81892e64f12..21f27f95770 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1254,11 +1254,12 @@ export class SyncApi { const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId()!)?.getSender(); - if (client.isCryptoEnabled()) { - const parkedHistory = await client.crypto!.cryptoStore.takeParkedSharedHistory(room.roomId); + const crypto = client.crypto; + if (crypto) { + const parkedHistory = await crypto.cryptoStore.takeParkedSharedHistory(room.roomId); for (const parked of parkedHistory) { if (parked.senderId === inviter) { - await client.crypto!.olmDevice.addInboundGroupSession( + await crypto.olmDevice.addInboundGroupSession( room.roomId, parked.senderKey, parked.forwardingCurve25519KeyChain, From e492a44ddec89d4627c5a2cda646cb909d70796b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 3 Feb 2023 14:01:53 +0000 Subject: [PATCH 36/45] Update typescript bits to aid matrix-react-sdk achieve noImplicitAny (#3079) --- .../{another-json.ts => another-json.d.ts} | 0 src/autodiscovery.ts | 2 +- src/client.ts | 2 ++ src/interactive-auth.ts | 16 ++++++++-------- 4 files changed, 11 insertions(+), 9 deletions(-) rename src/@types/{another-json.ts => another-json.d.ts} (100%) diff --git a/src/@types/another-json.ts b/src/@types/another-json.d.ts similarity index 100% rename from src/@types/another-json.ts rename to src/@types/another-json.d.ts diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index 0457175f389..f4a34159613 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -47,7 +47,7 @@ interface WellKnownConfig extends Omit { error?: IWellKnownConfig["error"] | null; } -interface ClientConfig extends Omit { +export interface ClientConfig extends Omit { "m.homeserver": WellKnownConfig; "m.identity_server": WellKnownConfig; } diff --git a/src/client.ts b/src/client.ts index ad00c95a909..a48d58e5d59 100644 --- a/src/client.ts +++ b/src/client.ts @@ -573,6 +573,8 @@ export interface IWellKnownConfig { error?: Error | string; // eslint-disable-next-line base_url?: string | null; + // XXX: this is undocumented + server_name?: string; } export interface IDelegatedAuthConfig { diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts index e4baefa6df2..7d9c183f59d 100644 --- a/src/interactive-auth.ts +++ b/src/interactive-auth.ts @@ -24,7 +24,7 @@ import { MatrixError } from "./http-api"; const EMAIL_STAGE_TYPE = "m.login.email.identity"; const MSISDN_STAGE_TYPE = "m.login.msisdn"; -interface IFlow { +export interface UIAFlow { stages: AuthType[]; } @@ -48,8 +48,8 @@ export interface IAuthData { session?: string; type?: string; completed?: string[]; - flows?: IFlow[]; - available_flows?: IFlow[]; + flows?: UIAFlow[]; + available_flows?: UIAFlow[]; stages?: string[]; required_stages?: AuthType[]; params?: Record>; @@ -101,7 +101,7 @@ class NoAuthFlowFoundError extends Error { public name = "NoAuthFlowFoundError"; // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase - public constructor(m: string, public readonly required_stages: string[], public readonly flows: IFlow[]) { + public constructor(m: string, public readonly required_stages: string[], public readonly flows: UIAFlow[]) { super(m); } } @@ -198,7 +198,7 @@ export class InteractiveAuth { private emailSid?: string; private requestingEmailToken = false; private attemptAuthDeferred: IDeferred | null = null; - private chosenFlow: IFlow | null = null; + private chosenFlow: UIAFlow | null = null; private currentStage: string | null = null; private emailAttempt = 1; @@ -320,7 +320,7 @@ export class InteractiveAuth { return this.data.params?.[loginType]; } - public getChosenFlow(): IFlow | null { + public getChosenFlow(): UIAFlow | null { return this.chosenFlow; } @@ -573,7 +573,7 @@ export class InteractiveAuth { * @returns flow * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found */ - private chooseFlow(): IFlow { + private chooseFlow(): UIAFlow { const flows = this.data.flows || []; // we've been given an email or we've already done an email part @@ -610,7 +610,7 @@ export class InteractiveAuth { * @internal * @returns login type */ - private firstUncompletedStage(flow: IFlow): AuthType | undefined { + private firstUncompletedStage(flow: UIAFlow): AuthType | undefined { const completed = this.data.completed || []; return flow.stages.find((stageType) => !completed.includes(stageType)); } From 05bf6428bc3e7cfa9055c9b4dbd07bb2ab432f75 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Feb 2023 15:58:50 +0000 Subject: [PATCH 37/45] Element-R: implement encryption of outgoing events (#3122) This PR wires up the Rust-SDK into the event encryption path --- package.json | 1 + spec/integ/crypto.spec.ts | 29 ++++ spec/unit/matrix-client.spec.ts | 5 +- spec/unit/rust-crypto/KeyClaimManager.spec.ts | 158 ++++++++++++++++++ src/client.ts | 22 +-- src/common-crypto/CryptoBackend.ts | 35 ++++ src/crypto/index.ts | 6 +- src/rust-crypto/KeyClaimManager.ts | 72 ++++++++ src/rust-crypto/RoomEncryptor.ts | 130 ++++++++++++++ src/rust-crypto/index.ts | 3 +- src/rust-crypto/rust-crypto.ts | 81 +++++++++ src/sliding-sync-sdk.ts | 4 +- src/sync.ts | 4 +- yarn.lock | 73 +++++++- 14 files changed, 598 insertions(+), 25 deletions(-) create mode 100644 spec/unit/rust-crypto/KeyClaimManager.spec.ts create mode 100644 src/rust-crypto/KeyClaimManager.ts create mode 100644 src/rust-crypto/RoomEncryptor.ts diff --git a/package.json b/package.json index 88668ba5781..0a8d7438fdd 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "eslint-plugin-unicorn": "^45.0.0", "exorcist": "^2.0.0", "fake-indexeddb": "^4.0.0", + "fetch-mock-jest": "^1.5.1", "jest": "^29.0.0", "jest-environment-jsdom": "^29.0.0", "jest-localstorage-mock": "^2.4.6", diff --git a/spec/integ/crypto.spec.ts b/spec/integ/crypto.spec.ts index 910987b455a..eb39f4fcff2 100644 --- a/spec/integ/crypto.spec.ts +++ b/spec/integ/crypto.spec.ts @@ -633,6 +633,35 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm (%s)", (backend: string, expect(event.getContent().body).toEqual("42"); }); + oldBackendOnly("prepareToEncrypt", async () => { + aliceTestClient.expectKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); + await aliceTestClient.start(); + aliceTestClient.client.setGlobalErrorOnUnknownDevices(false); + + // tell alice she is sharing a room with bob + aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"])); + await aliceTestClient.flushSync(); + + // we expect alice first to query bob's keys... + aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz")); + aliceTestClient.httpBackend.flush("/keys/query", 1); + + // ... and then claim one of his OTKs + aliceTestClient.httpBackend.when("POST", "/keys/claim").respond(200, getTestKeysClaimResponse("@bob:xyz")); + aliceTestClient.httpBackend.flush("/keys/claim", 1); + + // fire off the prepare request + const room = aliceTestClient.client.getRoom(ROOM_ID); + expect(room).toBeTruthy(); + const p = aliceTestClient.client.prepareToEncrypt(room!); + + // we expect to get a room key message + await expectSendRoomKey(aliceTestClient.httpBackend, "@bob:xyz", testOlmAccount); + + // the prepare request should complete successfully. + await p; + }); + oldBackendOnly("Alice sends a megolm message", async () => { aliceTestClient.expectKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); await aliceTestClient.start(); diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index e78d2eab19a..45a584c06bc 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -1415,7 +1415,7 @@ describe("MatrixClient", function () { expect(getRoomId).toEqual(roomId); return mockRoom; }; - client.crypto = { + client.crypto = client["cryptoBackend"] = { // mock crypto encryptEvent: () => new Promise(() => {}), stop: jest.fn(), @@ -1437,8 +1437,9 @@ describe("MatrixClient", function () { it("should cancel an event which is encrypting", async () => { // @ts-ignore protected method access - client.encryptAndSendEvent(null, event); + client.encryptAndSendEvent(mockRoom, event); await testUtils.emitPromise(event, "Event.status"); + expect(event.status).toBe(EventStatus.ENCRYPTING); client.cancelPendingEvent(event); assertCancelled(); }); diff --git a/spec/unit/rust-crypto/KeyClaimManager.spec.ts b/spec/unit/rust-crypto/KeyClaimManager.spec.ts new file mode 100644 index 00000000000..e448feabeca --- /dev/null +++ b/spec/unit/rust-crypto/KeyClaimManager.spec.ts @@ -0,0 +1,158 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; +import fetchMock from "fetch-mock-jest"; +import { Mocked } from "jest-mock"; +import { KeysClaimRequest, UserId } from "@matrix-org/matrix-sdk-crypto-js"; + +import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; +import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager"; +import { TypedEventEmitter } from "../../../src/models/typed-event-emitter"; +import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src"; + +afterEach(() => { + fetchMock.mockReset(); +}); + +describe("KeyClaimManager", () => { + /* for these tests, we connect a KeyClaimManager to a mock OlmMachine, and a real OutgoingRequestProcessor + * (which is connected to a mock fetch implementation) + */ + + /** the KeyClaimManager implementation under test */ + let keyClaimManager: KeyClaimManager; + + /** a mocked-up OlmMachine which the OutgoingRequestProcessor and KeyClaimManager are connected to */ + let olmMachine: Mocked; + + beforeEach(async () => { + const dummyEventEmitter = new TypedEventEmitter(); + const httpApi = new MatrixHttpApi(dummyEventEmitter, { + baseUrl: "https://example.com", + prefix: "/_matrix", + onlyData: true, + }); + + olmMachine = { + getMissingSessions: jest.fn(), + markRequestAsSent: jest.fn(), + } as unknown as Mocked; + + const outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, httpApi); + + keyClaimManager = new KeyClaimManager(olmMachine, outgoingRequestProcessor); + }); + + /** + * Returns a promise which resolve once olmMachine.markRequestAsSent is called. + * + * The call itself will block initially. + * + * The promise returned by this function yields a callback function, which should be called to unblock the + * markRequestAsSent call. + */ + function awaitCallToMarkRequestAsSent(): Promise<() => void> { + return new Promise<() => void>((resolveCalledPromise, _reject) => { + olmMachine.markRequestAsSent.mockImplementationOnce(async () => { + // the mock implementation returns a promise... + const completePromise = new Promise((resolveCompletePromise, _reject) => { + // ... and we now resolve the original promise with the resolver for that second promise. + resolveCalledPromise(resolveCompletePromise); + }); + return completePromise; + }); + }); + } + + it("should claim missing keys", async () => { + const u1 = new UserId("@alice:example.com"); + const u2 = new UserId("@bob:example.com"); + + // stub out olmMachine.getMissingSessions(), with a result indicating that it needs a keyclaim + const keysClaimRequest = new KeysClaimRequest("1234", '{ "k1": "v1" }'); + olmMachine.getMissingSessions.mockResolvedValueOnce(keysClaimRequest); + + // have the claim request return a 200 + fetchMock.postOnce("https://example.com/_matrix/client/v3/keys/claim", '{ "k": "v" }'); + + // also stub out olmMachine.markRequestAsSent + olmMachine.markRequestAsSent.mockResolvedValueOnce(undefined); + + // fire off the request + await keyClaimManager.ensureSessionsForUsers([u1, u2]); + + // check that all the calls were made + expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1, u2]); + expect(fetchMock).toHaveFetched("https://example.com/_matrix/client/v3/keys/claim", { + method: "POST", + body: { k1: "v1" }, + }); + expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", keysClaimRequest.type, '{ "k": "v" }'); + }); + + it("should wait for previous claims to complete before making another", async () => { + const u1 = new UserId("@alice:example.com"); + const u2 = new UserId("@bob:example.com"); + + // stub out olmMachine.getMissingSessions(), with a result indicating that it needs a keyclaim + const keysClaimRequest = new KeysClaimRequest("1234", '{ "k1": "v1" }'); + olmMachine.getMissingSessions.mockResolvedValue(keysClaimRequest); + + // have the claim request return a 200 + fetchMock.post("https://example.com/_matrix/client/v3/keys/claim", '{ "k": "v" }'); + + // stub out olmMachine.markRequestAsSent, and have it block + let markRequestAsSentPromise = awaitCallToMarkRequestAsSent(); + + // fire off two requests, and keep track of whether their promises resolve + let req1Resolved = false; + keyClaimManager.ensureSessionsForUsers([u1]).then(() => { + req1Resolved = true; + }); + let req2Resolved = false; + const req2 = keyClaimManager.ensureSessionsForUsers([u2]).then(() => { + req2Resolved = true; + }); + + // now: wait for the (first) call to OlmMachine.markRequestAsSent + let resolveMarkRequestAsSentCallback = await markRequestAsSentPromise; + + // at this point, there should have been a single call to getMissingSessions, and a single fetch; and neither + // call to ensureSessionsAsUsers should have completed + expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1]); + expect(olmMachine.getMissingSessions).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(req1Resolved).toBe(false); + expect(req2Resolved).toBe(false); + + // await the next call to markRequestAsSent, and release the first one + markRequestAsSentPromise = awaitCallToMarkRequestAsSent(); + resolveMarkRequestAsSentCallback(); + resolveMarkRequestAsSentCallback = await markRequestAsSentPromise; + + // the first request should now have completed, and we should have more calls and fetches + expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u2]); + expect(olmMachine.getMissingSessions).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(req1Resolved).toBe(true); + expect(req2Resolved).toBe(false); + + // finally, release the second call to markRequestAsSent and check that the second request completes + resolveMarkRequestAsSentCallback(); + await req2; + }); +}); diff --git a/src/client.ts b/src/client.ts index a48d58e5d59..88e17e9ac7b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2185,7 +2185,11 @@ export class MatrixClient extends TypedEventEmitter; + /** * Decrypt a received event * @@ -117,6 +138,20 @@ export interface SyncCryptoCallbacks { */ preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise; + /** + * Called by the /sync loop whenever an m.room.encryption event is received. + * + * This is called before RoomStateEvents are emitted for any of the events in the /sync + * response (even if the other events technically happened first). This works around a problem + * if the client uses a RoomStateEvent (typically a membership event) as a trigger to send a message + * in a new room (or one where encryption has been newly enabled): that would otherwise leave the + * crypto layer confused because it expects crypto to be set up, but it has not yet been. + * + * @param room - in which the event was received + * @param event - encryption event to be processed + */ + onCryptoEvent(room: Room, event: MatrixEvent): 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 b0a5783b32d..58ac18d06a4 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -2808,11 +2808,7 @@ export class Crypto extends TypedEventEmitter { - if (!room) { - throw new Error("Cannot send encrypted messages in unknown rooms"); - } - + public async encryptEvent(event: MatrixEvent, room: Room): Promise { const roomId = event.getRoomId()!; const alg = this.roomEncryptors.get(roomId); diff --git a/src/rust-crypto/KeyClaimManager.ts b/src/rust-crypto/KeyClaimManager.ts new file mode 100644 index 00000000000..0479bfa43c1 --- /dev/null +++ b/src/rust-crypto/KeyClaimManager.ts @@ -0,0 +1,72 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { OlmMachine, UserId } from "@matrix-org/matrix-sdk-crypto-js"; + +import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; + +/** + * KeyClaimManager: linearises calls to OlmMachine.getMissingSessions to avoid races + * + * We have one of these per `RustCrypto` (and hence per `MatrixClient`). + */ +export class KeyClaimManager { + private currentClaimPromise: Promise; + private stopped = false; + + public constructor( + private readonly olmMachine: OlmMachine, + private readonly outgoingRequestProcessor: OutgoingRequestProcessor, + ) { + this.currentClaimPromise = Promise.resolve(); + } + + /** + * Tell the KeyClaimManager to immediately stop processing requests. + * + * Any further calls, and any still in the queue, will fail with an error. + */ + public stop(): void { + this.stopped = true; + } + + /** + * Given a list of users, attempt to ensure that we have Olm Sessions active with each of their devices + * + * If we don't have an active olm session, we will claim a one-time key and start one. + * + * @param userList - list of userIDs to claim + */ + public ensureSessionsForUsers(userList: Array): Promise { + // The Rust-SDK requires that we only have one getMissingSessions process in flight at once. This little dance + // ensures that, by only having one call to ensureSessionsForUsersInner active at once (and making them + // queue up in order). + const prom = this.currentClaimPromise.finally(() => this.ensureSessionsForUsersInner(userList)); + this.currentClaimPromise = prom; + return prom; + } + + private async ensureSessionsForUsersInner(userList: Array): Promise { + // bail out quickly if we've been stopped. + if (this.stopped) { + throw new Error(`Cannot ensure Olm sessions: shutting down`); + } + const claimRequest = await this.olmMachine.getMissingSessions(userList); + if (claimRequest) { + await this.outgoingRequestProcessor.makeOutgoingRequest(claimRequest); + } + } +} diff --git a/src/rust-crypto/RoomEncryptor.ts b/src/rust-crypto/RoomEncryptor.ts new file mode 100644 index 00000000000..acd0e9ff033 --- /dev/null +++ b/src/rust-crypto/RoomEncryptor.ts @@ -0,0 +1,130 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { EncryptionSettings, OlmMachine, RoomId, UserId } from "@matrix-org/matrix-sdk-crypto-js"; + +import { EventType } from "../@types/event"; +import { IContent, MatrixEvent } from "../models/event"; +import { Room } from "../models/room"; +import { logger, PrefixedLogger } from "../logger"; +import { KeyClaimManager } from "./KeyClaimManager"; +import { RoomMember } from "../models/room-member"; + +/** + * RoomEncryptor: responsible for encrypting messages to a given room + */ +export class RoomEncryptor { + private readonly prefixedLogger: PrefixedLogger; + + /** + * @param olmMachine - The rust-sdk's OlmMachine + * @param keyClaimManager - Our KeyClaimManager, which manages the queue of one-time-key claim requests + * @param room - The room we want to encrypt for + * @param encryptionSettings - body of the m.room.encryption event currently in force in this room + */ + public constructor( + private readonly olmMachine: OlmMachine, + private readonly keyClaimManager: KeyClaimManager, + private readonly room: Room, + private encryptionSettings: IContent, + ) { + this.prefixedLogger = logger.withPrefix(`[${room.roomId} encryption]`); + } + + /** + * Handle a new `m.room.encryption` event in this room + * + * @param config - The content of the encryption event + */ + public onCryptoEvent(config: IContent): void { + if (JSON.stringify(this.encryptionSettings) != JSON.stringify(config)) { + this.prefixedLogger.error(`Ignoring m.room.encryption event which requests a change of config`); + } + } + + /** + * Handle a new `m.room.member` event in this room + * + * @param member - new membership state + */ + public onRoomMembership(member: RoomMember): void { + this.prefixedLogger.debug(`${member.membership} event for ${member.userId}`); + + if ( + member.membership == "join" || + (member.membership == "invite" && this.room.shouldEncryptForInvitedMembers()) + ) { + // make sure we are tracking the deviceList for this user + this.prefixedLogger.debug(`starting to track devices for: ${member.userId}`); + this.olmMachine.updateTrackedUsers([new UserId(member.userId)]); + } + + // TODO: handle leaves (including our own) + } + + /** + * Prepare to encrypt events in this room. + * + * This ensures that we have a megolm session ready to use and that we have shared its key with all the devices + * in the room. + */ + public async ensureEncryptionSession(): Promise { + if (this.encryptionSettings.algorithm !== "m.megolm.v1.aes-sha2") { + throw new Error( + `Cannot encrypt in ${this.room.roomId} for unsupported algorithm '${this.encryptionSettings.algorithm}'`, + ); + } + + const members = await this.room.getEncryptionTargetMembers(); + this.prefixedLogger.debug( + `Encrypting for users (shouldEncryptForInvitedMembers: ${this.room.shouldEncryptForInvitedMembers()}):`, + members.map((u) => `${u.userId} (${u.membership})`), + ); + + const userList = members.map((u) => new UserId(u.userId)); + await this.keyClaimManager.ensureSessionsForUsers(userList); + + const rustEncryptionSettings = new EncryptionSettings(); + /* FIXME historyVisibility, rotation, etc */ + + await this.olmMachine.shareRoomKey(new RoomId(this.room.roomId), userList, rustEncryptionSettings); + } + + /** + * Encrypt an event for this room + * + * This will ensure that we have a megolm session for this room, share it with the devices in the room, and + * then encrypt the event using the session. + * + * @param event - Event to be encrypted. + */ + public async encryptEvent(event: MatrixEvent): Promise { + await this.ensureEncryptionSession(); + + const encryptedContent = await this.olmMachine.encryptRoomEvent( + new RoomId(this.room.roomId), + event.getType(), + JSON.stringify(event.getContent()), + ); + + event.makeEncrypted( + EventType.RoomMessageEncrypted, + JSON.parse(encryptedContent), + this.olmMachine.identityKeys.curve25519.toBase64(), + this.olmMachine.identityKeys.ed25519.toBase64(), + ); + } +} diff --git a/src/rust-crypto/index.ts b/src/rust-crypto/index.ts index 4c826078f9f..7faeff15882 100644 --- a/src/rust-crypto/index.ts +++ b/src/rust-crypto/index.ts @@ -18,7 +18,6 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; import { RustCrypto } from "./rust-crypto"; import { logger } from "../logger"; -import { CryptoBackend } from "../common-crypto/CryptoBackend"; import { RUST_SDK_STORE_PREFIX } from "./constants"; import { IHttpOpts, MatrixHttpApi } from "../http-api"; @@ -26,7 +25,7 @@ export async function initRustCrypto( http: MatrixHttpApi, userId: string, deviceId: string, -): Promise { +): Promise { // initialise the rust matrix-sdk-crypto-js, if it hasn't already been done await RustSdkCryptoJs.initAsync(); diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 287ab71f415..2377b8a2177 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -20,11 +20,15 @@ import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypt import type { IToDeviceEvent } from "../sync-accumulator"; import type { IEncryptedEventInfo } from "../crypto/api"; import { MatrixEvent } from "../models/event"; +import { Room } from "../models/room"; +import { RoomMember } from "../models/room-member"; import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend"; import { logger } from "../logger"; import { IHttpOpts, MatrixHttpApi } from "../http-api"; import { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning"; +import { RoomEncryptor } from "./RoomEncryptor"; import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; +import { KeyClaimManager } from "./KeyClaimManager"; /** * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. @@ -39,6 +43,10 @@ export class RustCrypto implements CryptoBackend { /** whether {@link outgoingRequestLoop} is currently running */ private outgoingRequestLoopRunning = false; + /** mapping of roomId → encryptor class */ + private roomEncryptors: Record = {}; + + private keyClaimManager: KeyClaimManager; private outgoingRequestProcessor: OutgoingRequestProcessor; public constructor( @@ -48,8 +56,15 @@ export class RustCrypto implements CryptoBackend { _deviceId: string, ) { this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http); + this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor); } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // CryptoBackend implementation + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + public stop(): void { // stop() may be called multiple times, but attempting to close() the OlmMachine twice // will cause an error. @@ -58,12 +73,33 @@ export class RustCrypto implements CryptoBackend { } this.stopped = true; + this.keyClaimManager.stop(); + // 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 prepareToEncrypt(room: Room): void { + const encryptor = this.roomEncryptors[room.roomId]; + + if (encryptor) { + encryptor.ensureEncryptionSession(); + } + } + + public async encryptEvent(event: MatrixEvent, _room: Room): Promise { + const roomId = event.getRoomId()!; + const encryptor = this.roomEncryptors[roomId]; + + if (!encryptor) { + throw new Error(`Cannot encrypt event in unconfigured room ${roomId}`); + } + + await encryptor.encryptEvent(event); + } + public async decryptEvent(event: MatrixEvent): Promise { const roomId = event.getRoomId(); if (!roomId) { @@ -156,6 +192,30 @@ export class RustCrypto implements CryptoBackend { return JSON.parse(result); } + /** called by the sync loop on m.room.encrypted events + * + * @param room - in which the event was received + * @param event - encryption event to be processed + */ + public async onCryptoEvent(room: Room, event: MatrixEvent): Promise { + const config = event.getContent(); + + const existingEncryptor = this.roomEncryptors[room.roomId]; + if (existingEncryptor) { + existingEncryptor.onCryptoEvent(config); + } else { + this.roomEncryptors[room.roomId] = new RoomEncryptor(this.olmMachine, this.keyClaimManager, room, config); + } + + // start tracking devices for any users already known to be in this room. + const members = await room.getEncryptionTargetMembers(); + logger.debug( + `[${room.roomId} encryption] starting to track devices for: `, + members.map((u) => `${u.userId} (${u.membership})`), + ); + await this.olmMachine.updateTrackedUsers(members.map((u) => new RustSdkCryptoJs.UserId(u.userId))); + } + /** called by the sync loop after processing each sync. * * TODO: figure out something equivalent for sliding sync. @@ -168,6 +228,27 @@ export class RustCrypto implements CryptoBackend { this.outgoingRequestLoop(); } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // Other public functions + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** called by the MatrixClient on a room membership event + * + * @param event - The matrix event which caused this event to fire. + * @param member - The member whose RoomMember.membership changed. + * @param oldMembership - The previous membership state. Null if it's a new member. + */ + public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void { + const enc = this.roomEncryptors[event.getRoomId()!]; + if (!enc) { + // not encrypting in this room + return; + } + enc.onRoomMembership(member); + } + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Outgoing requests diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 91ff9d7a75e..f63a3046002 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -736,8 +736,8 @@ export class SlidingSyncSdk { const processRoomEvent = async (e: MatrixEvent): Promise => { client.emit(ClientEvent.Event, e); - if (e.isState() && e.getType() == EventType.RoomEncryption && this.syncOpts.crypto) { - await this.syncOpts.crypto.onCryptoEvent(room, e); + if (e.isState() && e.getType() == EventType.RoomEncryption && this.syncOpts.cryptoCallbacks) { + await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e); } }; diff --git a/src/sync.ts b/src/sync.ts index 21f27f95770..46b7367ca99 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1409,10 +1409,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.syncOpts.crypto) { + if (this.syncOpts.cryptoCallbacks) { for (const e of stateEvents.concat(events)) { if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") { - await this.syncOpts.crypto.onCryptoEvent(room, e); + await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e); } } } diff --git a/yarn.lock b/yarn.lock index d9cc1ac4a37..089afda3da9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,7 +63,7 @@ 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": +"@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.5": version "7.20.12" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== @@ -988,7 +988,7 @@ pirates "^4.0.5" source-map-support "^0.5.16" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": version "7.20.13" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== @@ -2840,6 +2840,11 @@ core-js@^2.4.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== +core-js@^3.0.0: + version "3.27.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.27.2.tgz#85b35453a424abdcacb97474797815f4d62ebbf7" + integrity sha512-9ashVQskuh5AZEZ1JdQWp1GqSoC1e1G87MzRqg2gIfVAQ7Qn9K+uFj8EcniUFA4P2NLZfV+TOlX1SzoKfo+s7w== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -3694,6 +3699,29 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fetch-mock-jest@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/fetch-mock-jest/-/fetch-mock-jest-1.5.1.tgz#0e13df990d286d9239e284f12b279ed509bf53cd" + integrity sha512-+utwzP8C+Pax1GSka3nFXILWMY3Er2L+s090FOgqVNrNCPp0fDqgXnAHAJf12PLHi0z4PhcTaZNTz8e7K3fjqQ== + dependencies: + fetch-mock "^9.11.0" + +fetch-mock@^9.11.0: + version "9.11.0" + resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-9.11.0.tgz#371c6fb7d45584d2ae4a18ee6824e7ad4b637a3f" + integrity sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q== + dependencies: + "@babel/core" "^7.0.0" + "@babel/runtime" "^7.0.0" + core-js "^3.0.0" + debug "^4.1.1" + glob-to-regexp "^0.4.0" + is-subset "^0.1.1" + lodash.isequal "^4.5.0" + path-to-regexp "^2.2.1" + querystring "^0.2.0" + whatwg-url "^6.5.0" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -3873,6 +3901,11 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob-to-regexp@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + glob@^7.1.0, glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -4366,6 +4399,11 @@ is-string@^1.0.5, is-string@^1.0.7: dependencies: has-tostringtag "^1.0.0" +is-subset@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + integrity sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw== + is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" @@ -5127,6 +5165,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -5673,6 +5716,11 @@ path-platform@~0.11.15: resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" integrity sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg== +path-to-regexp@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704" + integrity sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -5953,6 +6001,11 @@ querystring@0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== +querystring@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" + integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== + querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -6764,6 +6817,13 @@ tough-cookie@^4.1.2: universalify "^0.2.0" url-parse "^1.5.3" +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== + dependencies: + punycode "^2.1.0" + tr46@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" @@ -7227,6 +7287,15 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +whatwg-url@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-url@^8.4.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" From 2a363598dd98e3de0079633e95f4aea7fff49fe9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 3 Feb 2023 16:16:32 +0000 Subject: [PATCH 38/45] Element-R: fix a bug which prevented encryption working after a reload (#3126) Upgrade matrix-sdk-crypto-js, to pick up https://github.com/matrix-org/matrix-rust-sdk/pull/1429 --- yarn.lock | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 089afda3da9..c4d8980e297 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1399,13 +1399,12 @@ lodash "^4.17.21" "@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.3": - version "0.1.0-alpha.3" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.3.tgz#829ea03bcad8051dc1e4f0da18a66d4ba273f78f" - integrity sha512-KpEddjC34aobFlUYf2mIaXqkjLC0goRmYnbDZLTd0MwiFtau4b1TrPptQ8XFc90Z2VeAcvf18CBqA2otmZzUKQ== + version "0.1.0-alpha.4" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.4.tgz#1b20294e0354c3dcc9c7dc810d883198a4042f04" + integrity sha512-mdaDKrw3P5ZVCpq0ioW0pV6ihviDEbS8ZH36kpt9stLKHwwDSopPogE6CkQhi0B1jn1yBUtOYi32mBV/zcOR7g== "@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": From 8a3d7d5671e5caa707179fdfa671ef14db363ef9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 6 Feb 2023 10:50:05 +0000 Subject: [PATCH 39/45] Element-R: log outgoing HTTP requests (#3127) otherwise it's rather hard to see them, at least in Firefox. --- src/rust-crypto/OutgoingRequestProcessor.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/rust-crypto/OutgoingRequestProcessor.ts b/src/rust-crypto/OutgoingRequestProcessor.ts index c0163f8525c..7ac9a2105db 100644 --- a/src/rust-crypto/OutgoingRequestProcessor.ts +++ b/src/rust-crypto/OutgoingRequestProcessor.ts @@ -104,6 +104,13 @@ export class OutgoingRequestProcessor { prefix: "", }; - return await this.http.authedRequest(method, path, queryParams, body, opts); + try { + const response = await this.http.authedRequest(method, path, queryParams, body, opts); + logger.info(`rust-crypto: successfully made HTTP request: ${method} ${path}`); + return response; + } catch (e) { + logger.warn(`rust-crypto: error making HTTP request: ${method} ${path}: ${e}`); + throw e; + } } } From 71cf812d24a623712995d0aa8dee28dd1a561e5d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Feb 2023 10:08:03 +0000 Subject: [PATCH 40/45] Add @typescript-eslint/no-base-to-string (#3129) --- .eslintrc.js | 3 +++ package.json | 2 +- src/crypto/algorithms/megolm.ts | 5 +++-- src/crypto/index.ts | 1 + yarn.lock | 8 ++++---- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index fd77e6c5e42..18b502d7f98 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,9 @@ module.exports = { plugins: ["matrix-org", "import", "jsdoc"], extends: ["plugin:matrix-org/babel", "plugin:import/typescript"], + parserOptions: { + project: ["./tsconfig.json"], + }, env: { browser: true, node: true, diff --git a/package.json b/package.json index 0a8d7438fdd..9ec8c63cd2d 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "eslint-import-resolver-typescript": "^3.5.1", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "eslint-plugin-matrix-org": "^0.9.0", + "eslint-plugin-matrix-org": "^0.10.0", "eslint-plugin-tsdoc": "^0.2.17", "eslint-plugin-unicorn": "^45.0.0", "exorcist": "^2.0.0", diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index ff7e29264c8..3037dec1351 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -1344,7 +1344,7 @@ export class MegolmDecryption extends DecryptionAlgorithm { errorCode = "OLM_UNKNOWN_MESSAGE_INDEX"; } - throw new DecryptionError(errorCode, e ? e.toString() : "Unknown Error: Error is undefined", { + throw new DecryptionError(errorCode, e instanceof Error ? e.message : "Unknown Error: Error is undefined", { session: content.sender_key + "|" + content.session_id, }); } @@ -1367,7 +1367,8 @@ export class MegolmDecryption extends DecryptionAlgorithm { if (problem) { this.prefixedLogger.info( `When handling UISI from ${event.getSender()} (sender key ${content.sender_key}): ` + - `recent session problem with that sender: ${problem}`, + `recent session problem with that sender:`, + problem, ); let problemDescription = PROBLEM_DESCRIPTIONS[problem.type as "no_olm"] || PROBLEM_DESCRIPTIONS.unknown; if (problem.fixed) { diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 58ac18d06a4..64e7d607c11 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -1202,6 +1202,7 @@ export class Crypto extends TypedEventEmitter): Promise { if (!(key instanceof Uint8Array)) { + // eslint-disable-next-line @typescript-eslint/no-base-to-string throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`); } const pickleKey = Buffer.from(this.olmDevice.pickleKey); diff --git a/yarn.lock b/yarn.lock index c4d8980e297..4da6b5d0355 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3385,10 +3385,10 @@ eslint-plugin-jsdoc@^39.6.4: semver "^7.3.8" spdx-expression-parse "^3.0.1" -eslint-plugin-matrix-org@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.9.0.tgz#b2a5186052ddbfa7dc9878779bafa5d68681c7b4" - integrity sha512-+j6JuMnFH421Z2vOxc+0YMt5Su5vD76RSatviy3zHBaZpgd+sOeAWoCLBHD5E7mMz5oKae3Y3wewCt9LRzq2Nw== +eslint-plugin-matrix-org@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.10.0.tgz#8d0998641a4d276343cae2abf253a01bb4d4cc60" + integrity sha512-L7ail0x1yUlF006kn4mHc+OT8/aYZI++i852YXPHxCbM1EY7jeg/fYAQ8tCx5+x08LyqXeS7inAVSL784m0C6Q== eslint-plugin-tsdoc@^0.2.17: version "0.2.17" From e2a694115fbf9e8b6d7efb89fb0ab34aea880738 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 7 Feb 2023 12:00:44 +0000 Subject: [PATCH 41/45] Prepare changelog for v23.3.0-rc.1 --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48434193aeb..a02c61953b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +Changes in [23.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.3.0-rc.1) (2023-02-07) +============================================================================================================ + +## ✨ Features + * Element-R: implement encryption of outgoing events ([\#3122](https://github.com/matrix-org/matrix-js-sdk/pull/3122)). + * Poll model - page /relations results ([\#3073](https://github.com/matrix-org/matrix-js-sdk/pull/3073)). Contributed by @kerryarchibald. + * Poll model - validate end events ([\#3072](https://github.com/matrix-org/matrix-js-sdk/pull/3072)). Contributed by @kerryarchibald. + * Handle optional last_known_event_id property in m.predecessor ([\#3119](https://github.com/matrix-org/matrix-js-sdk/pull/3119)). Contributed by @andybalaam. + * Add support for stable identifier for fixed MAC in SAS verification ([\#3101](https://github.com/matrix-org/matrix-js-sdk/pull/3101)). + * Provide eventId as well as roomId from Room.findPredecessor ([\#3095](https://github.com/matrix-org/matrix-js-sdk/pull/3095)). Contributed by @andybalaam. + * MSC3946 Dynamic room predecessors ([\#3042](https://github.com/matrix-org/matrix-js-sdk/pull/3042)). Contributed by @andybalaam. + * Poll model ([\#3036](https://github.com/matrix-org/matrix-js-sdk/pull/3036)). Contributed by @kerryarchibald. + * Remove video tracks on video mute without renegotiating ([\#3091](https://github.com/matrix-org/matrix-js-sdk/pull/3091)). + * Introduces a backwards-compatible API change. `MegolmEncrypter#prepareToEncrypt`'s return type has changed from `void` to `() => void`. ([\#3035](https://github.com/matrix-org/matrix-js-sdk/pull/3035)). Contributed by @clarkf. + +## 🐛 Bug Fixes + * Element-R: fix a bug which prevented encryption working after a reload ([\#3126](https://github.com/matrix-org/matrix-js-sdk/pull/3126)). + * Element-R: Fix invite processing ([\#3121](https://github.com/matrix-org/matrix-js-sdk/pull/3121)). + * Don't throw with no `opponentDeviceInfo` ([\#3107](https://github.com/matrix-org/matrix-js-sdk/pull/3107)). + * Remove flaky megolm test ([\#3098](https://github.com/matrix-org/matrix-js-sdk/pull/3098)). Contributed by @clarkf. + * Fix "verifyLinks" functionality of getRoomUpgradeHistory ([\#3089](https://github.com/matrix-org/matrix-js-sdk/pull/3089)). Contributed by @andybalaam. + Changes in [23.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.2.0) (2023-01-31) ================================================================================================== From f61db819611d635a712c7f93ab7e66b883b0e2bc Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 7 Feb 2023 12:00:46 +0000 Subject: [PATCH 42/45] v23.3.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 9ec8c63cd2d..65e9c4fae19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "23.2.0", + "version": "23.3.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": "./src/browser-index.ts", "matrix_src_main": "./src/index.ts", "matrix_src_browser": "./src/browser-index.ts", @@ -144,5 +144,6 @@ "outputDirectory": "coverage", "outputName": "jest-sonar-report.xml", "relativePaths": true - } + }, + "typings": "./lib/index.d.ts" } From 6dda9e532da26c9e681adf7c3f1e4b23a5e82731 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Tue, 14 Feb 2023 09:58:17 +0000 Subject: [PATCH 43/45] Include 'browser' in list of adjusted properties in release.sh (#3149) (#3151) (cherry picked from commit 5e17626fe00fbd6e49873ddc3d2532eef64b9d1a) Co-authored-by: Andy Balaam --- post-release.sh | 2 +- release.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/post-release.sh b/post-release.sh index f42c1d6e067..50e9579b4dc 100755 --- a/post-release.sh +++ b/post-release.sh @@ -11,7 +11,7 @@ jq --version > /dev/null || (echo "jq is required: please install it"; kill $$) if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then # When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously. - for i in main typings + for i in main typings browser do # If a `lib` prefixed value is present, it means we adjusted the field # earlier at publish time, so we should revert it now. diff --git a/release.sh b/release.sh index c5f798f400b..6da627294ef 100755 --- a/release.sh +++ b/release.sh @@ -180,7 +180,7 @@ yarn version --no-git-tag-version --new-version "$release" # they exist). This small bit of gymnastics allows us to use the TypeScript # source directly for development without needing to build before linting or # testing. -for i in main typings +for i in main typings browser do lib_value=$(jq -r ".matrix_lib_$i" package.json) if [ "$lib_value" != "null" ]; then From f81b7e5e6f7c0d9e4d56b36213fc6eb7e3d7b3f5 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 14 Feb 2023 10:21:21 +0000 Subject: [PATCH 44/45] Prepare changelog for v23.3.0 --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a02c61953b7..354bef7e060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ -Changes in [23.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.3.0-rc.1) (2023-02-07) -============================================================================================================ +Changes in [23.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.3.0) (2023-02-14) +================================================================================================== ## ✨ Features * Element-R: implement encryption of outgoing events ([\#3122](https://github.com/matrix-org/matrix-js-sdk/pull/3122)). @@ -14,6 +14,9 @@ Changes in [23.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta * Introduces a backwards-compatible API change. `MegolmEncrypter#prepareToEncrypt`'s return type has changed from `void` to `() => void`. ([\#3035](https://github.com/matrix-org/matrix-js-sdk/pull/3035)). Contributed by @clarkf. ## 🐛 Bug Fixes + * Stop the ICE disconnected timer on call terminate ([\#3147](https://github.com/matrix-org/matrix-js-sdk/pull/3147)). + * Clear notifications when we can infer read status from receipts ([\#3139](https://github.com/matrix-org/matrix-js-sdk/pull/3139)). Fixes vector-im/element-web#23991. + * Messages sent out of order after one message fails ([\#3131](https://github.com/matrix-org/matrix-js-sdk/pull/3131)). Fixes vector-im/element-web#22885 and vector-im/element-web#18942. Contributed by @justjanne. * Element-R: fix a bug which prevented encryption working after a reload ([\#3126](https://github.com/matrix-org/matrix-js-sdk/pull/3126)). * Element-R: Fix invite processing ([\#3121](https://github.com/matrix-org/matrix-js-sdk/pull/3121)). * Don't throw with no `opponentDeviceInfo` ([\#3107](https://github.com/matrix-org/matrix-js-sdk/pull/3107)). From 182534288cac90bfad9b20c40219b10bc051c0fa Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 14 Feb 2023 10:21:24 +0000 Subject: [PATCH 45/45] v23.3.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 65e9c4fae19..a445c452633 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "23.3.0-rc.1", + "version": "23.3.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=16.0.0" @@ -33,7 +33,7 @@ "matrix-org" ], "main": "./lib/index.js", - "browser": "./src/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",