Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f61db81961 | |||
| e2a694115f | |||
| 71cf812d24 | |||
| 8a3d7d5671 | |||
| 2a363598dd | |||
| 05bf6428bc | |||
| e492a44dde | |||
| 44d2e47f96 | |||
| 8f5db463e7 | |||
| 4e8affafcc | |||
| 2800681bb1 | |||
| b2a9e6f12f | |||
| fbd2c97f87 | |||
| 6c6304a620 | |||
| 0c1d5f6b25 | |||
| 1c26dc0233 | |||
| a163a202e7 | |||
| 4f918f684e | |||
| c142232f4d | |||
| c9bc20aa4d | |||
| 5c0cb3a536 | |||
| 415576d0a0 | |||
| 4f9fad66e4 | |||
| ebd9854980 | |||
| cb2fab64d8 | |||
| f446b49e49 | |||
| 22f5e41058 | |||
| fee5a006f1 | |||
| ef51ee28fd | |||
| cb61345780 | |||
| a18d4e226e | |||
| b09b33eb4c | |||
| 5fedc06d7c | |||
| d8c9f6db33 | |||
| 0af5fa0328 | |||
| b328b72cd5 | |||
| ce2a9d7036 | |||
| 66ae985af5 | |||
| 1828e2849c | |||
| b4f8b0fe4f | |||
| 70656e954f | |||
| 40a4c8d954 | |||
| 7ed787b86a | |||
| 1ee487a2ff | |||
| b76e7ca782 | |||
| ddce1bcd28 |
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
+8
-7
@@ -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"
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -98,24 +98,25 @@
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"prettier": "2.8.3",
|
||||
"rimraf": "^4.0.0",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typedoc": "^0.23.20",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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<EmittedEvents, Emitt
|
||||
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
|
||||
public getRoom = jest.fn();
|
||||
|
||||
public supportsExperimentalThreads(): boolean {
|
||||
public supportsThreads(): boolean {
|
||||
return true;
|
||||
}
|
||||
public async decryptEventIfNeeded(): Promise<void> {}
|
||||
|
||||
@@ -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<Crypto>;
|
||||
|
||||
// @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,78 @@ describe("MegolmDecryption", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepareToEncrypt", () => {
|
||||
let megolm: MegolmEncryptionClass;
|
||||
let room: jest.Mocked<Room>;
|
||||
|
||||
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>;
|
||||
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("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 () {
|
||||
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
|
||||
+357
-29
@@ -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<IMatrixClientCreateOpts>) {
|
||||
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) => {
|
||||
@@ -1414,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(),
|
||||
@@ -1436,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();
|
||||
});
|
||||
@@ -1456,9 +1458,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 +1500,7 @@ describe("MatrixClient", function () {
|
||||
expect(threadEvents).toHaveLength(1);
|
||||
|
||||
// Restore method
|
||||
client.supportsExperimentalThreads = supportsExperimentalThreads;
|
||||
client.supportsThreads = supportsThreads;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2205,7 +2218,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",
|
||||
@@ -2234,7 +2247,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,42 +2359,164 @@ 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", () => {
|
||||
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)]);
|
||||
if (creates) {
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)]);
|
||||
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)]);
|
||||
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)]);
|
||||
}
|
||||
|
||||
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], {});
|
||||
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.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];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -2334,6 +2540,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 +2594,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 +2647,42 @@ 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,
|
||||
]);
|
||||
});
|
||||
|
||||
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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
/*
|
||||
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, 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, 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";
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
mockClient.relations.mockReset().mockResolvedValue({ events: [] });
|
||||
|
||||
maySendRedactionForEventSpy.mockClear().mockReturnValue(true);
|
||||
});
|
||||
|
||||
let eventId = 1;
|
||||
const makeRelatedEvent = (eventProps: Partial<IEvent>, 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, room);
|
||||
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, room)).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, room)).toThrowError("Invalid poll start event.");
|
||||
});
|
||||
|
||||
describe("fetching responses", () => {
|
||||
it("calls relations api and emits", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const emitSpy = jest.spyOn(poll, "emit");
|
||||
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);
|
||||
});
|
||||
|
||||
it("returns existing responses object after initial fetch", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
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, room);
|
||||
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, room);
|
||||
const responses = await poll.getResponses();
|
||||
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" });
|
||||
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, 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("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);
|
||||
});
|
||||
|
||||
it("sets poll end event with unstable event type", async () => {
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [unstablePollEndEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
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, room);
|
||||
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, room);
|
||||
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, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
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("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);
|
||||
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, room);
|
||||
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, room);
|
||||
// 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, room);
|
||||
// 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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
+155
-12
@@ -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,
|
||||
@@ -1622,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,
|
||||
});
|
||||
@@ -2468,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();
|
||||
@@ -2539,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();
|
||||
@@ -2606,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();
|
||||
@@ -2646,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();
|
||||
@@ -2687,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);
|
||||
|
||||
@@ -2794,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);
|
||||
|
||||
@@ -3228,12 +3230,84 @@ 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(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3266,21 +3340,90 @@ describe("Room", function () {
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
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();
|
||||
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", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([
|
||||
roomCreateEvent("roomid", "replacedroomid"),
|
||||
predecessorEvent("roomid", "otherreplacedroomid"),
|
||||
]);
|
||||
const useMsc3946 = true;
|
||||
expect(room.findPredecessor(useMsc3946)).toEqual({
|
||||
roomId: "otherreplacedroomid",
|
||||
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",
|
||||
});
|
||||
});
|
||||
|
||||
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.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", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([
|
||||
roomCreateEvent("roomid", null), // Create event has no predecessor
|
||||
predecessorEvent("roomid", "otherreplacedroomid", "lastevtid"),
|
||||
]);
|
||||
// Don't provide an argument for msc3946ProcessDynamicPredecessor -
|
||||
// we should ignore the predecessor event.
|
||||
expect(room.findPredecessor()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||
baseUrl: "https://example.com",
|
||||
prefix: "/_matrix",
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
olmMachine = {
|
||||
getMissingSessions: jest.fn(),
|
||||
markRequestAsSent: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
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<void>((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;
|
||||
});
|
||||
});
|
||||
@@ -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<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
/** wait for a call to olmMachine.markRequestAsSent */
|
||||
function awaitCallToMarkAsSent(): Promise<void> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
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<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
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, "");
|
||||
});
|
||||
});
|
||||
@@ -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<OutgoingRequestProcessor>;
|
||||
|
||||
/** a mocked-up OlmMachine which rustCrypto is connected to */
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
@@ -116,28 +108,25 @@ describe("RustCrypto", () => {
|
||||
* the front of the queue, until it is empty. */
|
||||
let outgoingRequestQueue: Array<Array<any>>;
|
||||
|
||||
/** wait for a call to olmMachine.markRequestAsSent */
|
||||
function awaitCallToMarkAsSent(): Promise<void> {
|
||||
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<void>((resolveCompletePromise, _reject) => {
|
||||
resolveCalledPromise(resolveCompletePromise);
|
||||
});
|
||||
return completePromise;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
|
||||
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
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<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
rustCrypto = new RustCrypto(olmMachine, httpApi, TEST_USER, TEST_DEVICE_ID);
|
||||
outgoingRequestProcessor = {
|
||||
makeOutgoingRequest: jest.fn(),
|
||||
} as unknown as Mocked<OutgoingRequestProcessor>;
|
||||
|
||||
rustCrypto = new RustCrypto(olmMachine, {} as MatrixHttpApi<any>, TEST_USER, TEST_DEVICE_ID);
|
||||
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
|
||||
});
|
||||
|
||||
it("should poll for outgoing messages", () => {
|
||||
it("should poll for outgoing messages and send them", async () => {
|
||||
const testReq = new KeysQueryRequest("1234", "{}");
|
||||
outgoingRequestQueue.push([testReq]);
|
||||
|
||||
const makeRequestPromise = awaitCallToMakeOutgoingRequest();
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
await makeRequestPromise;
|
||||
expect(olmMachine.outgoingRequests).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/* simple requests that map directly to the request body */
|
||||
const tests: Array<[any, "POST" | "PUT", string]> = [
|
||||
[KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
|
||||
[KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
|
||||
[KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
|
||||
[SignatureUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/signatures/upload"],
|
||||
[KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
|
||||
];
|
||||
|
||||
for (const [RequestClass, expectedMethod, expectedPath] of tests) {
|
||||
it(`should handle ${RequestClass.name}s`, async () => {
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new RequestClass("1234", testBody);
|
||||
outgoingRequestQueue.push([outgoingRequest]);
|
||||
|
||||
const testResponse = '{ "result": 1 }';
|
||||
httpBackend
|
||||
.when(expectedMethod, "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual(expectedPath);
|
||||
expect(req.rawData).toEqual(testBody);
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
|
||||
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await markSentCallPromise;
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
}
|
||||
|
||||
it("does not explode with unknown requests", async () => {
|
||||
const outgoingRequest = { id: "5678", type: 987 };
|
||||
outgoingRequestQueue.push([outgoingRequest]);
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
await awaitCallToMarkAsSent();
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
|
||||
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
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,9 @@ import {
|
||||
MockMediaStreamTrack,
|
||||
installWebRTCMocks,
|
||||
MockRTCPeerConnection,
|
||||
MockRTCRtpTransceiver,
|
||||
SCREENSHARE_STREAM_ID,
|
||||
MockRTCRtpSender,
|
||||
} from "../../test-utils/webrtc";
|
||||
import { CallFeed } from "../../../src/webrtc/callFeed";
|
||||
import { EventType, IContent, ISendEventResponse, MatrixEvent, Room } from "../../../src";
|
||||
@@ -536,8 +538,15 @@ 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
|
||||
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,
|
||||
);
|
||||
@@ -829,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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -47,7 +47,7 @@ interface WellKnownConfig extends Omit<IWellKnownConfig, "error"> {
|
||||
error?: IWellKnownConfig["error"] | null;
|
||||
}
|
||||
|
||||
interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
|
||||
export interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
|
||||
"m.homeserver": WellKnownConfig;
|
||||
"m.identity_server": WellKnownConfig;
|
||||
}
|
||||
|
||||
+136
-75
@@ -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.
|
||||
@@ -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";
|
||||
@@ -446,10 +445,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
|
||||
*/
|
||||
@@ -568,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 {
|
||||
@@ -1448,6 +1455,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
|
||||
}
|
||||
|
||||
if (this.clientOpts.hasOwnProperty("experimentalThreadSupport")) {
|
||||
logger.warn("`experimentalThreadSupport` has been deprecated, use `threadSupport` instead");
|
||||
}
|
||||
|
||||
// If `threadSupport` is omitted and the deprecated `experimentalThreadSupport` has been passed
|
||||
// We should fallback to that value for backwards compatibility purposes
|
||||
if (
|
||||
!this.clientOpts.hasOwnProperty("threadSupport") &&
|
||||
this.clientOpts.hasOwnProperty("experimentalThreadSupport")
|
||||
) {
|
||||
this.clientOpts.threadSupport = this.clientOpts.experimentalThreadSupport;
|
||||
}
|
||||
|
||||
this.syncApi.sync();
|
||||
|
||||
if (this.clientOpts.clientWellKnownPollPeriod !== undefined) {
|
||||
@@ -2165,7 +2185,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
|
||||
// needed.
|
||||
const RustCrypto = await import("./rust-crypto");
|
||||
this.cryptoBackend = await RustCrypto.initRustCrypto(this.http, userId, deviceId);
|
||||
const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId);
|
||||
this.cryptoBackend = rustCrypto;
|
||||
|
||||
// attach the event listeners needed by RustCrypto
|
||||
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2588,10 +2612,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param room - the room the event is in
|
||||
*/
|
||||
public prepareToEncrypt(room: Room): void {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
this.crypto.prepareToEncrypt(room);
|
||||
this.cryptoBackend.prepareToEncrypt(room);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3780,14 +3804,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* This is essentially getRooms() with some rooms filtered out, eg. old versions
|
||||
* of rooms that have been replaced or (in future) other rooms that have been
|
||||
* marked at the protocol level as not to be displayed to the user.
|
||||
*
|
||||
* @param msc3946ProcessDynamicPredecessor - if true, look for an
|
||||
* m.room.predecessor state event and
|
||||
* use it if found (MSC3946).
|
||||
* @returns A list of rooms, or an empty list if there is no data store.
|
||||
*/
|
||||
public getVisibleRooms(): Room[] {
|
||||
public getVisibleRooms(msc3946ProcessDynamicPredecessor = false): Room[] {
|
||||
const allRooms = this.store.getRooms();
|
||||
|
||||
const replacedRooms = new Set();
|
||||
for (const r of allRooms) {
|
||||
const predecessor = r.findPredecessorRoomId();
|
||||
const predecessor = r.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
if (predecessor) {
|
||||
replacedRooms.add(predecessor);
|
||||
}
|
||||
@@ -4368,11 +4396,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.isRoomEncrypted(event.getRoomId()!)) {
|
||||
if (!room || !this.isRoomEncrypted(event.getRoomId()!)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.crypto && this.usingExternalCrypto) {
|
||||
if (!this.cryptoBackend && this.usingExternalCrypto) {
|
||||
// The client has opted to allow sending messages to encrypted
|
||||
// rooms even if the room is encrypted, and we haven't setup
|
||||
// crypto. This is useful for users of matrix-org/pantalaimon
|
||||
@@ -4393,13 +4421,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.crypto) {
|
||||
throw new Error(
|
||||
"This room is configured to use encryption, but your client does " + "not support encryption.",
|
||||
);
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("This room is configured to use encryption, but your client does not support encryption.");
|
||||
}
|
||||
|
||||
return this.crypto.encryptEvent(event, room);
|
||||
return this.cryptoBackend.encryptEvent(event, room);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4817,8 +4843,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
$eventId: event.getId()!,
|
||||
});
|
||||
|
||||
const supportsThreadRR = this.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
|
||||
if (supportsThreadRR && !unthreaded) {
|
||||
if (!unthreaded) {
|
||||
const isThread = !!event.threadRootId;
|
||||
body = {
|
||||
...body,
|
||||
@@ -4986,70 +5011,83 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* which can be proven to be linked. For example, rooms which have a create
|
||||
* event pointing to an old room which the client is not aware of or doesn't
|
||||
* have a matching tombstone would not be returned.
|
||||
* @param msc3946ProcessDynamicPredecessor - if true, look for
|
||||
* m.room.predecessor state events as well as create events, and prefer
|
||||
* predecessor events where they exist (MSC3946).
|
||||
* @returns An array of rooms representing the upgrade
|
||||
* history.
|
||||
*/
|
||||
public getRoomUpgradeHistory(roomId: string, verifyLinks = false): Room[] {
|
||||
let currentRoom = this.getRoom(roomId);
|
||||
public getRoomUpgradeHistory(
|
||||
roomId: string,
|
||||
verifyLinks = false,
|
||||
msc3946ProcessDynamicPredecessor = false,
|
||||
): Room[] {
|
||||
const currentRoom = this.getRoom(roomId);
|
||||
if (!currentRoom) return [];
|
||||
|
||||
const upgradeHistory = [currentRoom];
|
||||
const before = this.findPredecessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor);
|
||||
const after = this.findSuccessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor);
|
||||
|
||||
// Work backwards first, looking at create events.
|
||||
let createEvent = currentRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
while (createEvent) {
|
||||
const predecessor = createEvent.getContent()["predecessor"];
|
||||
if (predecessor && predecessor["room_id"]) {
|
||||
const refRoom = this.getRoom(predecessor["room_id"]);
|
||||
if (!refRoom) break; // end of the chain
|
||||
return [...before, currentRoom, ...after];
|
||||
}
|
||||
|
||||
if (verifyLinks) {
|
||||
const tombstone = refRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
private findPredecessorRooms(room: Room, verifyLinks: boolean, msc3946ProcessDynamicPredecessor: boolean): Room[] {
|
||||
const ret: Room[] = [];
|
||||
|
||||
if (!tombstone || tombstone.getContent()["replacement_room"] !== refRoom.roomId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at the front because we're working backwards from the currentRoom
|
||||
upgradeHistory.splice(0, 0, refRoom);
|
||||
createEvent = refRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
} else {
|
||||
// No further create events to look at
|
||||
// Work backwards from newer to older rooms
|
||||
let predecessorRoomId = room.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
while (predecessorRoomId !== null) {
|
||||
const predecessorRoom = this.getRoom(predecessorRoomId);
|
||||
if (predecessorRoom === null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (verifyLinks) {
|
||||
const tombstone = predecessorRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
if (!tombstone || tombstone.getContent()["replacement_room"] !== room.roomId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Work forwards next, looking at tombstone events
|
||||
let tombstoneEvent = currentRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
// Insert at the front because we're working backwards from the currentRoom
|
||||
ret.splice(0, 0, predecessorRoom);
|
||||
|
||||
room = predecessorRoom;
|
||||
predecessorRoomId = room.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private findSuccessorRooms(room: Room, verifyLinks: boolean, msc3946ProcessDynamicPredecessor: boolean): Room[] {
|
||||
const ret: Room[] = [];
|
||||
|
||||
// Work forwards, looking at tombstone events
|
||||
let tombstoneEvent = room.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
while (tombstoneEvent) {
|
||||
const refRoom = this.getRoom(tombstoneEvent.getContent()["replacement_room"]);
|
||||
if (!refRoom) break; // end of the chain
|
||||
if (refRoom.roomId === currentRoom.roomId) break; // Tombstone is referencing it's own room
|
||||
const successorRoom = this.getRoom(tombstoneEvent.getContent()["replacement_room"]);
|
||||
if (!successorRoom) break; // end of the chain
|
||||
if (successorRoom.roomId === room.roomId) break; // Tombstone is referencing its own room
|
||||
|
||||
if (verifyLinks) {
|
||||
createEvent = refRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
if (!createEvent || !createEvent.getContent()["predecessor"]) break;
|
||||
|
||||
const predecessor = createEvent.getContent()["predecessor"];
|
||||
if (predecessor["room_id"] !== currentRoom.roomId) break;
|
||||
const predecessorRoomId = successorRoom.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
if (!predecessorRoomId || predecessorRoomId !== room.roomId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Push to the end because we're looking forwards
|
||||
upgradeHistory.push(refRoom);
|
||||
const roomIds = new Set(upgradeHistory.map((ref) => 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5420,7 +5458,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
|
||||
this.processBeaconEvents(room, timelineEvents);
|
||||
this.processAggregatedTimelineEvents(room, timelineEvents);
|
||||
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||
this.processThreadEvents(room, threadedEvents, true);
|
||||
|
||||
@@ -5481,7 +5519,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return timelineSet.getTimelineForEvent(eventId);
|
||||
}
|
||||
|
||||
if (timelineSet.thread && this.supportsExperimentalThreads()) {
|
||||
if (timelineSet.thread && this.supportsThreads()) {
|
||||
return this.getThreadTimeline(timelineSet, eventId);
|
||||
}
|
||||
|
||||
@@ -5535,7 +5573,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
|
||||
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
|
||||
this.processThreadEvents(timelineSet.room, threadedEvents, true);
|
||||
this.processBeaconEvents(timelineSet.room, timelineEvents);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents);
|
||||
|
||||
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
|
||||
// timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
|
||||
@@ -5548,7 +5586,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
public async getThreadTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | undefined> {
|
||||
if (!this.supportsExperimentalThreads()) {
|
||||
if (!this.supportsThreads()) {
|
||||
throw new Error("could not get thread timeline: no client support");
|
||||
}
|
||||
|
||||
@@ -5630,7 +5668,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
|
||||
timeline.setPaginationToken(resNewer.next_batch ?? null, Direction.Forward);
|
||||
this.processBeaconEvents(timelineSet.room, events);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, events);
|
||||
|
||||
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
|
||||
// timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
|
||||
@@ -5687,7 +5725,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
|
||||
timeline.setPaginationToken(null, Direction.Forward);
|
||||
this.processBeaconEvents(timelineSet.room, events);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, events);
|
||||
|
||||
return timeline;
|
||||
}
|
||||
@@ -5940,7 +5978,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// in the notification timeline set
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(timelineSet.room, matrixEvents);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
@@ -5982,7 +6020,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(room, matrixEvents);
|
||||
this.processAggregatedTimelineEvents(room, matrixEvents);
|
||||
this.processThreadRoots(room, matrixEvents, backwards);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
@@ -6029,7 +6067,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const originalEvent = await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id);
|
||||
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, eventTimeline, null);
|
||||
}
|
||||
this.processBeaconEvents(timelineSet.room, matrixEvents);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
@@ -6067,7 +6105,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
const [timelineEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(room, timelineEvents);
|
||||
this.processAggregatedTimelineEvents(room, timelineEvents);
|
||||
this.processThreadRoots(
|
||||
room,
|
||||
timelineEvents.filter((it) => it.getServerAggregatedRelation(THREAD_RELATION_TYPE.name)),
|
||||
@@ -7009,10 +7047,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const serverVersions = await this.serverVersionsPromise;
|
||||
this.canSupport = await buildFeatureSupportMap(serverVersions);
|
||||
|
||||
// We can set flag values to use their stable or unstable version
|
||||
const support = this.canSupport.get(Feature.ThreadUnreadNotifications);
|
||||
UNREAD_THREAD_NOTIFICATIONS.setPreferUnstable(support === ServerSupport.Unstable);
|
||||
|
||||
return this.serverVersionsPromise;
|
||||
}
|
||||
|
||||
@@ -9320,12 +9354,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @deprecated use supportsThreads() instead
|
||||
*/
|
||||
public supportsExperimentalThreads(): boolean {
|
||||
logger.warn(`supportsExperimentalThreads() is deprecated, use supportThreads() instead`);
|
||||
return this.clientOpts?.experimentalThreadSupport || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to determine thread support
|
||||
* @returns a boolean to determine if threads are enabled
|
||||
*/
|
||||
public supportsThreads(): boolean {
|
||||
return this.clientOpts?.threadSupport || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the summary of a room as defined by an initial version of MSC3266 and implemented in Synapse
|
||||
* Proposed at https://github.com/matrix-org/matrix-doc/pull/3266
|
||||
@@ -9340,24 +9383,42 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Processes a list of threaded events and adds them to their respective timelines
|
||||
* @param room - the room the adds the threaded events
|
||||
* @param threadedEvents - an array of the threaded events
|
||||
* @param toStartOfTimeline - the direction in which we want to add the events
|
||||
*/
|
||||
public processThreadEvents(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
room.processThreadedEvents(threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Processes a list of thread roots and creates a thread model
|
||||
* @param room - the room to create the threads in
|
||||
* @param threadedEvents - an array of thread roots
|
||||
* @param toStartOfTimeline - the direction
|
||||
*/
|
||||
public processThreadRoots(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
room.processThreadRoots(threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
public processBeaconEvents(room?: Room, events?: MatrixEvent[]): void {
|
||||
this.processAggregatedTimelineEvents(room, events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls aggregation functions for event types that are aggregated
|
||||
* Polls and location beacons
|
||||
* @param room - room the events belong to
|
||||
* @param events - timeline events to be processed
|
||||
* @returns
|
||||
*/
|
||||
public processAggregatedTimelineEvents(room?: Room, events?: MatrixEvent[]): void {
|
||||
if (!events?.length) return;
|
||||
if (!room) return;
|
||||
|
||||
room.currentState.processBeaconEvents(events, this);
|
||||
room.processPollEvents(events);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypt
|
||||
import type { IToDeviceEvent } from "../sync-accumulator";
|
||||
import type { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { IEncryptedEventInfo } from "../crypto/api";
|
||||
|
||||
/**
|
||||
@@ -75,6 +76,26 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
*/
|
||||
checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel;
|
||||
|
||||
/**
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
*
|
||||
* @param room - the room the event is in
|
||||
*/
|
||||
prepareToEncrypt(room: Room): void;
|
||||
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room.
|
||||
*
|
||||
* @param event - event to be sent
|
||||
*
|
||||
* @param room - destination room.
|
||||
*
|
||||
* @returns Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
encryptEvent(event: MatrixEvent, room: Room): Promise<void>;
|
||||
|
||||
/**
|
||||
* Decrypt a received event
|
||||
*
|
||||
@@ -117,6 +138,20 @@ export interface SyncCryptoCallbacks {
|
||||
*/
|
||||
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
|
||||
|
||||
/**
|
||||
* 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<void>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop after each /sync response is processed.
|
||||
*
|
||||
|
||||
@@ -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<T = DeviceInfo> {
|
||||
deviceInfo: T;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IOutboundGroupSessionKey {
|
||||
chain_index: number;
|
||||
key: string;
|
||||
@@ -106,7 +106,6 @@ interface IPayload extends Partial<IMessage> {
|
||||
algorithm?: string;
|
||||
sender_key?: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface SharedWithData {
|
||||
// The identity key of the device we shared with
|
||||
@@ -223,6 +222,7 @@ export class MegolmEncryption extends EncryptionAlgorithm {
|
||||
private encryptionPreparation?: {
|
||||
promise: Promise<void>;
|
||||
startTime: number;
|
||||
cancel: () => void;
|
||||
};
|
||||
|
||||
protected readonly roomId: string;
|
||||
@@ -974,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<void> => {
|
||||
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
|
||||
@@ -1016,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1165,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<null | [DeviceInfoMap, IBlockedMap]>;
|
||||
private async getDevicesInRoom(
|
||||
room: Room,
|
||||
forceDistributeToUnverified = false,
|
||||
): Promise<[DeviceInfoMap, IBlockedMap]> {
|
||||
isCancelled?: () => boolean,
|
||||
): Promise<null | [DeviceInfoMap, IBlockedMap]> {
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
this.prefixedLogger.debug(
|
||||
`Encrypting for users (shouldEncryptForInvitedMembers: ${room.shouldEncryptForInvitedMembers()}):`,
|
||||
@@ -1201,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)) {
|
||||
@@ -1213,6 +1248,11 @@ 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
|
||||
if (isCancelled !== undefined) await immediate();
|
||||
if (isCancelled?.() === true) return null;
|
||||
const deviceTrust = this.crypto.checkDeviceTrust(userId, deviceId);
|
||||
|
||||
if (
|
||||
@@ -1304,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,
|
||||
});
|
||||
}
|
||||
@@ -1327,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) {
|
||||
|
||||
+2
-5
@@ -1202,6 +1202,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*/
|
||||
public async storeSessionBackupPrivateKey(key: ArrayLike<number>): Promise<void> {
|
||||
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);
|
||||
@@ -2808,11 +2809,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* @returns Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
public async encryptEvent(event: MatrixEvent, room?: Room): Promise<void> {
|
||||
if (!room) {
|
||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||
}
|
||||
|
||||
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
|
||||
const roomId = event.getRoomId()!;
|
||||
|
||||
const alg = this.roomEncryptors.get(roomId);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string, Record<string, any>>;
|
||||
@@ -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<IAuthData> | 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));
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export * from "./models/event";
|
||||
export * from "./models/room";
|
||||
export * from "./models/event-timeline";
|
||||
export * from "./models/event-timeline-set";
|
||||
export * from "./models/poll";
|
||||
export * from "./models/room-member";
|
||||
export * from "./models/room-state";
|
||||
export * from "./models/user";
|
||||
|
||||
+5
-6
@@ -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.
|
||||
@@ -267,7 +267,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
private txnId?: string;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* A reference to the thread this event belongs to
|
||||
*/
|
||||
private thread?: Thread;
|
||||
@@ -546,7 +545,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Get the event ID of the thread head
|
||||
*/
|
||||
public get threadRootId(): string | undefined {
|
||||
@@ -559,7 +557,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* A helper to check if an event is a thread's head or not
|
||||
*/
|
||||
public get isThreadRoot(): boolean {
|
||||
const threadDetails = this.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
@@ -1557,7 +1555,8 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Set the instance of a thread associated with the current event
|
||||
* @param thread - the thread
|
||||
*/
|
||||
public setThread(thread?: Thread): void {
|
||||
if (this.thread) {
|
||||
@@ -1571,7 +1570,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Get the instance of the thread associated with the current event
|
||||
*/
|
||||
public getThread(): Thread | undefined {
|
||||
return this.thread;
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
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 { 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";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
export enum PollEvent {
|
||||
New = "Poll.new",
|
||||
End = "Poll.end",
|
||||
Update = "Poll.update",
|
||||
Responses = "Poll.Responses",
|
||||
Destroy = "Poll.Destroy",
|
||||
}
|
||||
|
||||
export type PollEventHandlerMap = {
|
||||
[PollEvent.Update]: (event: MatrixEvent, poll: Poll) => 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<Exclude<PollEvent, PollEvent.New>, PollEventHandlerMap> {
|
||||
public readonly roomId: string;
|
||||
public readonly pollEvent: PollStartEvent;
|
||||
private _isFetchingResponses = false;
|
||||
private relationsNextBatch: string | undefined;
|
||||
private responses: null | Relations = null;
|
||||
private endEvent: MatrixEvent | undefined;
|
||||
|
||||
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.");
|
||||
}
|
||||
this.roomId = this.rootEvent.getRoomId()!;
|
||||
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 get isFetchingResponses(): boolean {
|
||||
return this._isFetchingResponses;
|
||||
}
|
||||
|
||||
public async getResponses(): Promise<Relations> {
|
||||
// if we have already fetched some responses
|
||||
// just return them
|
||||
if (this.responses) {
|
||||
return this.responses;
|
||||
}
|
||||
|
||||
// if there is no fetching in progress
|
||||
// start fetching
|
||||
if (!this.isFetchingResponses) {
|
||||
await this.fetchResponses();
|
||||
}
|
||||
// return whatever responses we got from the first page
|
||||
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.validateEndEvent(event)) {
|
||||
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<void> {
|
||||
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",
|
||||
undefined,
|
||||
{
|
||||
from: this.relationsNextBatch || undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const responses =
|
||||
this.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 pollCloseTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER;
|
||||
|
||||
const { responseEvents } = filterResponseRelations(allRelations.events, pollCloseTimestamp);
|
||||
|
||||
responseEvents.forEach((event) => {
|
||||
responses.addEvent(event);
|
||||
});
|
||||
|
||||
this.relationsNextBatch = allRelations.nextBatch ?? undefined;
|
||||
this.responses = responses;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
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))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<RelationsEvent, EventHandlerMap
|
||||
*
|
||||
* @param event - The relation event to remove.
|
||||
*/
|
||||
private async removeEvent(event: MatrixEvent): Promise<void> {
|
||||
public async removeEvent(event: MatrixEvent): Promise<void> {
|
||||
if (!this.relations.has(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -964,6 +964,55 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
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"];
|
||||
let eventId = predecessorEvent.getContent()["last_known_event_id"];
|
||||
if (typeof eventId !== "string") {
|
||||
eventId = null;
|
||||
}
|
||||
if (typeof roomId === "string") {
|
||||
return { roomId, eventId };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
+68
-35
@@ -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,
|
||||
@@ -64,7 +64,7 @@ import {
|
||||
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 +162,8 @@ export type RoomEmittedEvents =
|
||||
| BeaconEvent.New
|
||||
| BeaconEvent.Update
|
||||
| BeaconEvent.Destroy
|
||||
| BeaconEvent.LivenessChange;
|
||||
| BeaconEvent.LivenessChange
|
||||
| PollEvent.New;
|
||||
|
||||
export type RoomEventHandlerMap = {
|
||||
/**
|
||||
@@ -289,6 +290,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<ThreadHandlerMap, ThreadEvent.Update | ThreadEvent.NewReply | ThreadEvent.Delete> &
|
||||
EventTimelineSetHandlerMap &
|
||||
Pick<MatrixEventHandlerMap, MatrixEventEvent.BeforeRedaction> &
|
||||
@@ -317,6 +323,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
*/
|
||||
private unthreadedReceipts = new Map<string, Receipt>();
|
||||
private readonly timelineSets: EventTimelineSet[];
|
||||
public readonly polls: Map<string, Poll> = new Map<string, Poll>();
|
||||
public readonly threadsTimelineSets: EventTimelineSet[] = [];
|
||||
// any filtered timeline sets we're maintaining for this room
|
||||
private readonly filteredTimelineSets: Record<string, EventTimelineSet> = {}; // filter_id: timelineSet
|
||||
@@ -375,7 +382,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
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<string, Thread>();
|
||||
public lastThread?: Thread;
|
||||
@@ -475,7 +483,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
return this.threadTimelineSetsPromise;
|
||||
}
|
||||
|
||||
if (this.client?.supportsExperimentalThreads()) {
|
||||
if (this.client?.supportsThreads()) {
|
||||
try {
|
||||
this.threadTimelineSetsPromise = Promise.all([
|
||||
this.createThreadTimelineSet(),
|
||||
@@ -1290,10 +1298,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@@ -1322,7 +1328,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @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'
|
||||
@@ -1334,7 +1339,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Checks if the current room has unread thread notifications
|
||||
* @returns
|
||||
*/
|
||||
@@ -1348,7 +1352,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @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'
|
||||
@@ -1369,7 +1372,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @returns the notification count type for all the threads in the room
|
||||
*/
|
||||
public get threadsAggregateNotificationType(): NotificationCountType | null {
|
||||
@@ -1385,7 +1387,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Resets the thread notifications for this room
|
||||
*/
|
||||
public resetThreadUnreadNotificationCount(notificationsToKeep?: string[]): void {
|
||||
@@ -1545,14 +1546,16 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @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());
|
||||
@@ -1819,7 +1822,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* Without server support that means fetching as much at once as the server allows us to.
|
||||
*/
|
||||
public async fetchRoomThreads(): Promise<void> {
|
||||
if (this.threadsReady || !this.client.supportsExperimentalThreads()) {
|
||||
if (this.threadsReady || !this.client.supportsThreads()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1890,6 +1893,38 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
this.threadsReady = true;
|
||||
}
|
||||
|
||||
public async processPollEvents(events: MatrixEvent[]): Promise<void> {
|
||||
const processPollStartEvent = (event: MatrixEvent): void => {
|
||||
if (!M_POLL_START.matches(event.getType())) return;
|
||||
try {
|
||||
const poll = new Poll(event, this.client, this);
|
||||
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
|
||||
@@ -1964,7 +1999,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
shouldLiveInThread: boolean;
|
||||
threadId?: string;
|
||||
} {
|
||||
if (!this.client?.supportsExperimentalThreads()) {
|
||||
if (!this.client?.supportsThreads()) {
|
||||
return {
|
||||
shouldLiveInRoom: true,
|
||||
shouldLiveInThread: false,
|
||||
@@ -2033,7 +2068,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
|
||||
/**
|
||||
* Adds events to a thread's timeline. Will fire "Thread.update"
|
||||
* @experimental
|
||||
*/
|
||||
public processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
events.forEach(this.applyRedaction);
|
||||
@@ -2662,7 +2696,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
// 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) => {
|
||||
@@ -2991,26 +3025,25 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the ID of the room that was this room's predecessor, or null if
|
||||
* this room has no predecessor.
|
||||
* 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
|
||||
* 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 findPredecessorRoomId(): string | null {
|
||||
public findPredecessor(
|
||||
msc3946ProcessDynamicPredecessor = false,
|
||||
): { roomId: string; eventId: string | null } | null {
|
||||
const currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
if (!currentState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const createEvent = currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
if (createEvent) {
|
||||
const predecessor = createEvent.getContent()["predecessor"];
|
||||
if (predecessor) {
|
||||
const roomId = predecessor["room_id"];
|
||||
if (roomId) {
|
||||
return roomId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return currentState.findPredecessor(msc3946ProcessDynamicPredecessor);
|
||||
}
|
||||
|
||||
private roomNameGenerator(state: RoomNameState): string {
|
||||
|
||||
@@ -69,9 +69,6 @@ export function determineFeatureSupport(stable: boolean, unstable: boolean): Fea
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
public static hasServerSideSupport = FeatureSupport.None;
|
||||
public static hasServerSideListSupport = FeatureSupport.None;
|
||||
|
||||
@@ -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<void>;
|
||||
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<UserId>): Promise<void> {
|
||||
// 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<UserId>): Promise<void> {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
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<IHttpOpts & { onlyData: true }>,
|
||||
) {}
|
||||
|
||||
public async makeOutgoingRequest(msg: OutgoingRequest): Promise<void> {
|
||||
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<string> {
|
||||
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: "",
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.http.authedRequest<string>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<IHttpOpts & { onlyData: true }>,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
): Promise<CryptoBackend> {
|
||||
): Promise<RustCrypto> {
|
||||
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
|
||||
|
||||
+100
-67
@@ -15,32 +15,20 @@ 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";
|
||||
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, 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 { RoomEncryptor } from "./RoomEncryptor";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
import { KeyClaimManager } from "./KeyClaimManager";
|
||||
|
||||
/**
|
||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||
@@ -55,12 +43,27 @@ export class RustCrypto implements CryptoBackend {
|
||||
/** whether {@link outgoingRequestLoop} is currently running */
|
||||
private outgoingRequestLoopRunning = false;
|
||||
|
||||
/** mapping of roomId → encryptor class */
|
||||
private roomEncryptors: Record<string, RoomEncryptor> = {};
|
||||
|
||||
private keyClaimManager: KeyClaimManager;
|
||||
private outgoingRequestProcessor: OutgoingRequestProcessor;
|
||||
|
||||
public constructor(
|
||||
private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
|
||||
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
_userId: string,
|
||||
_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
|
||||
@@ -70,13 +73,43 @@ 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<void> {
|
||||
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<IEventDecryptionResult> {
|
||||
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(),
|
||||
@@ -87,7 +120,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,
|
||||
@@ -159,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<void> {
|
||||
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.
|
||||
@@ -171,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
|
||||
@@ -190,7 +268,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 +277,4 @@ export class RustCrypto implements CryptoBackend {
|
||||
this.outgoingRequestLoopRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async doOutgoingRequest(msg: OutgoingRequest): Promise<void> {
|
||||
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<string> {
|
||||
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<string>(method, path, queryParams, body, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,8 +736,8 @@ export class SlidingSyncSdk {
|
||||
|
||||
const processRoomEvent = async (e: MatrixEvent): Promise<void> => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+8
-7
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
@@ -1408,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -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<T>(ms: number, value?: T): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise/async version of {@link setImmediate}.
|
||||
*/
|
||||
export function immediate(): Promise<void> {
|
||||
return new Promise(setImmediate);
|
||||
}
|
||||
|
||||
export function isNullOrUndefined(val: any): boolean {
|
||||
return val === null || val === undefined;
|
||||
}
|
||||
|
||||
+62
-20
@@ -387,6 +387,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
private opponentSessionId?: string;
|
||||
public groupCallId?: string;
|
||||
|
||||
// Used to keep the timer for the delay before actually stopping our
|
||||
// video track after muting (see setLocalVideoMuted)
|
||||
private stopVideoTrackTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
/**
|
||||
* Construct a new Matrix Call.
|
||||
* @param opts - Config options.
|
||||
@@ -480,7 +484,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
|
||||
public get type(): CallType {
|
||||
return this.hasLocalUserMediaVideoTrack || this.hasRemoteUserMediaVideoTrack ? CallType.Video : CallType.Voice;
|
||||
// we may want to look for a video receiver here rather than a track to match the
|
||||
// sender behaviour, although in practice they should be the same thing
|
||||
return this.hasUserMediaVideoSender || this.hasRemoteUserMediaVideoTrack ? CallType.Video : CallType.Voice;
|
||||
}
|
||||
|
||||
public get hasLocalUserMediaVideoTrack(): boolean {
|
||||
@@ -503,6 +509,14 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
});
|
||||
}
|
||||
|
||||
private get hasUserMediaAudioSender(): boolean {
|
||||
return Boolean(this.transceivers.get(getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, "audio"))?.sender);
|
||||
}
|
||||
|
||||
private get hasUserMediaVideoSender(): boolean {
|
||||
return Boolean(this.transceivers.get(getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, "video"))?.sender);
|
||||
}
|
||||
|
||||
public get localUsermediaFeed(): CallFeed | undefined {
|
||||
return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia);
|
||||
}
|
||||
@@ -1292,18 +1306,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.localUsermediaStream!.addTrack(track);
|
||||
}
|
||||
|
||||
// Secondly, we remove tracks that we no longer need from the peer
|
||||
// connection, if any. This only happens when we mute the video atm.
|
||||
// This will change the transceiver direction to "inactive" and
|
||||
// therefore cause re-negotiation.
|
||||
for (const kind of ["audio", "video"]) {
|
||||
const sender = this.transceivers.get(getTransceiverKey(SDPStreamMetadataPurpose.Usermedia, kind))?.sender;
|
||||
// Only remove the track if we aren't going to immediately replace it
|
||||
if (sender && !stream.getTracks().find((t) => 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<CallEvent, CallEventHandlerMap
|
||||
*/
|
||||
public async setLocalVideoMuted(muted: boolean): Promise<boolean> {
|
||||
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<CallEvent, CallEventHandlerMap
|
||||
return this.isMicrophoneMuted();
|
||||
}
|
||||
|
||||
if (!this.hasLocalUserMediaAudioTrack && !muted) {
|
||||
if (!muted && (!this.hasUserMediaAudioSender || !this.hasLocalUserMediaAudioTrack)) {
|
||||
await this.upgradeCall(true, false);
|
||||
return this.isMicrophoneMuted();
|
||||
}
|
||||
@@ -1496,6 +1528,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.localUsermediaStream!.id
|
||||
} micShouldBeMuted ${micShouldBeMuted} vidShouldBeMuted ${vidShouldBeMuted}`,
|
||||
);
|
||||
|
||||
setTracksEnabled(this.localUsermediaStream!.getAudioTracks(), !micShouldBeMuted);
|
||||
setTracksEnabled(this.localUsermediaStream!.getVideoTracks(), !vidShouldBeMuted);
|
||||
}
|
||||
@@ -2302,11 +2335,16 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
const userId = this.invitee || this.getOpponentMember()!.userId;
|
||||
if (this.client.getUseE2eForGroupCall()) {
|
||||
if (!this.opponentDeviceInfo) {
|
||||
logger.warn(`Call ${this.callId} sendVoipEvent() failed: we do not have opponentDeviceInfo`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.client.encryptAndSendToDevices(
|
||||
[
|
||||
{
|
||||
userId,
|
||||
deviceInfo: this.opponentDeviceInfo!,
|
||||
deviceInfo: this.opponentDeviceInfo,
|
||||
},
|
||||
],
|
||||
{
|
||||
@@ -2477,6 +2515,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
clearInterval(this.callLengthInterval);
|
||||
this.callLengthInterval = undefined;
|
||||
}
|
||||
if (this.stopVideoTrackTimer !== undefined) {
|
||||
clearTimeout(this.stopVideoTrackTimer);
|
||||
this.stopVideoTrackTimer = undefined;
|
||||
}
|
||||
|
||||
for (const [stream, listener] of this.removeTrackListeners) {
|
||||
stream.removeEventListener("removetrack", listener);
|
||||
|
||||
@@ -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,10 +988,10 @@
|
||||
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":
|
||||
version "7.20.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd"
|
||||
integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==
|
||||
"@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==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
@@ -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" "*"
|
||||
@@ -1398,14 +1398,13 @@
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.2":
|
||||
version "0.1.0-alpha.2"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6"
|
||||
integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA==
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.3":
|
||||
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":
|
||||
@@ -1574,6 +1573,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 +1693,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"
|
||||
@@ -1755,10 +1759,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"
|
||||
@@ -1771,20 +1775,20 @@
|
||||
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" "*"
|
||||
|
||||
"@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 +1797,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:
|
||||
@@ -2038,7 +2042,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==
|
||||
@@ -2054,7 +2058,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==
|
||||
@@ -2064,6 +2068,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"
|
||||
@@ -2825,6 +2839,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"
|
||||
@@ -2938,13 +2957,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"
|
||||
@@ -3169,26 +3181,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"
|
||||
@@ -3197,7 +3215,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"
|
||||
@@ -3293,18 +3322,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"
|
||||
@@ -3314,7 +3344,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==
|
||||
@@ -3322,28 +3352,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"
|
||||
@@ -3353,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"
|
||||
@@ -3426,10 +3458,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"
|
||||
@@ -3585,7 +3617,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==
|
||||
@@ -3655,6 +3698,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"
|
||||
@@ -3789,9 +3855,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"
|
||||
@@ -3834,6 +3900,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"
|
||||
@@ -3858,6 +3929,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"
|
||||
@@ -3930,6 +4008,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"
|
||||
@@ -4118,7 +4201,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==
|
||||
@@ -4135,6 +4218,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"
|
||||
@@ -4179,7 +4271,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==
|
||||
@@ -4306,6 +4398,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"
|
||||
@@ -4313,7 +4410,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==
|
||||
@@ -4489,15 +4586,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 +4692,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 +4717,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 +4871,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"
|
||||
@@ -5067,6 +5164,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"
|
||||
@@ -5309,11 +5411,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"
|
||||
@@ -5401,9 +5498,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"
|
||||
@@ -5420,7 +5517,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==
|
||||
@@ -5618,6 +5715,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"
|
||||
@@ -5683,10 +5785,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"
|
||||
@@ -5698,12 +5800,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"
|
||||
|
||||
@@ -5898,6 +6000,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"
|
||||
@@ -6148,7 +6255,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==
|
||||
@@ -6189,6 +6296,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"
|
||||
@@ -6704,6 +6816,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"
|
||||
@@ -6836,6 +6955,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"
|
||||
@@ -7158,6 +7286,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"
|
||||
@@ -7178,7 +7315,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==
|
||||
|
||||
Reference in New Issue
Block a user