From 2cda229bc4a5fbbb7399c3758cce14c35d7fb98e Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 19 Aug 2022 17:33:07 +0100 Subject: [PATCH 01/48] Automatically reconnect sessions when sliding sync expires them This can happen when you close your laptop overnight, as the server will not hold onto in-memory resources for your connection indefinitely. When this happen, the server will HTTP 400 you with "session expired". At this point, it is no longer safe to remember anything and you must forget everything and resend any sticky parameters. This commit does the sticky parameters and re-establishes the connection, but it may need additional work to make the JS SDK forget now invalid data. --- spec/integ/sliding-sync.spec.ts | 97 +++++++++++++++++++++++++++++++++ src/sliding-sync.ts | 33 ++++++++++- 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 8c4a7ad12..0464459c1 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -80,6 +80,103 @@ describe("SlidingSync", () => { slidingSync.stop(); httpBackend.verifyNoOutstandingExpectation(); }); + + it("should reset the connection on HTTP 400 and send everything again", async () => { + // seed the connection with some lists, extensions and subscriptions to verify they are sent again + slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1); + const roomId = "!sub:localhost"; + const subInfo = { + timeline_limit: 42, + required_state: [["m.room.create", ""]], + }; + const listInfo = { + ranges: [[0,10]], + filters: { + is_dm: true, + }, + }; + const ext = { + name: () => "custom_extension", + onRequest: (initial) => { return { initial: initial } }, + onResponse: (res) => { return {} }, + when: () => ExtensionState.PreProcess, + }; + slidingSync.modifyRoomSubscriptions(new Set([roomId])); + slidingSync.modifyRoomSubscriptionInfo(subInfo); + slidingSync.setList(0, listInfo); + slidingSync.registerExtension(ext); + slidingSync.start(); + + // expect everything to be sent + let txnId; + httpBackend.when("POST", syncUrl).check(function(req) { + const body = req.data; + logger.debug("got ", body); + expect(body.room_subscriptions).toEqual({ + [roomId]: subInfo, + }); + expect(body.lists[0]).toEqual(listInfo); + expect(body.extensions).toBeTruthy(); + expect(body.extensions["custom_extension"]).toEqual({initial:true}); + txnId = body.txn_id; + }).respond(200, function() { + return { + pos: "11", + lists: [{ count: 5 }], + extensions: {}, + txn_id: txnId, + }; + }); + await httpBackend.flushAllExpected(); + + // expect nothing but ranges and non-initial extensions to be sent + httpBackend.when("POST", syncUrl).check(function(req) { + const body = req.data; + logger.debug("got ", body); + expect(body.room_subscriptions).toBeFalsy(); + expect(body.lists[0]).toEqual({ + ranges: [[0,10]], + }); + expect(body.extensions).toBeTruthy(); + expect(body.extensions["custom_extension"]).toEqual({initial:false}); + }).respond(200, function() { + return { + pos: "12", + lists: [{ count: 5 }], + extensions: {}, + }; + }); + await httpBackend.flushAllExpected(); + + // now we expire the session + httpBackend.when("POST", syncUrl).respond(400, function() { + logger.debug("sending session expired 400"); + return { + error: "HTTP 400 : session expired", + }; + }); + await httpBackend.flushAllExpected(); + + // ...and everything should be sent again + httpBackend.when("POST", syncUrl).check(function(req) { + const body = req.data; + logger.debug("got ", body); + expect(body.room_subscriptions).toEqual({ + [roomId]: subInfo, + }); + expect(body.lists[0]).toEqual(listInfo); + expect(body.extensions).toBeTruthy(); + expect(body.extensions["custom_extension"]).toEqual({initial:true}); + }).respond(200, function() { + return { + pos: "1", + lists: [{ count: 6 }], + extensions: {}, + }; + }); + await httpBackend.flushAllExpected(); + slidingSync.stop(); + }); }); describe("room subscriptions", () => { diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 28026b3a9..5218004d0 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -694,6 +694,26 @@ export class SlidingSync extends TypedEventEmitter { + d.reject(d.txnId); + }); + this.txnIdDefers = []; + // resend sticky params and de-confirm all subscriptions + this.lists.forEach((l) => { + l.setModified(true); + }); + this.confirmedRoomSubscriptions = new Set(); // leave desired ones alone though! + // reset the connection as we might be wedged + this.needsResend = true; + this.pendingReq?.abort(); + } + /** * Start syncing with the server. Blocks until stopped. */ @@ -732,7 +752,6 @@ export class SlidingSync extends TypedEventEmitter Date: Fri, 19 Aug 2022 18:15:33 +0100 Subject: [PATCH 02/48] Linting --- spec/integ/sliding-sync.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 0464459c1..90354ca38 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -90,15 +90,15 @@ describe("SlidingSync", () => { required_state: [["m.room.create", ""]], }; const listInfo = { - ranges: [[0,10]], + ranges: [[0, 10]], filters: { is_dm: true, }, }; const ext = { name: () => "custom_extension", - onRequest: (initial) => { return { initial: initial } }, - onResponse: (res) => { return {} }, + onRequest: (initial) => { return { initial: initial }; }, + onResponse: (res) => { return {}; }, when: () => ExtensionState.PreProcess, }; slidingSync.modifyRoomSubscriptions(new Set([roomId])); @@ -117,7 +117,7 @@ describe("SlidingSync", () => { }); expect(body.lists[0]).toEqual(listInfo); expect(body.extensions).toBeTruthy(); - expect(body.extensions["custom_extension"]).toEqual({initial:true}); + expect(body.extensions["custom_extension"]).toEqual({ initial: true }); txnId = body.txn_id; }).respond(200, function() { return { @@ -135,10 +135,10 @@ describe("SlidingSync", () => { logger.debug("got ", body); expect(body.room_subscriptions).toBeFalsy(); expect(body.lists[0]).toEqual({ - ranges: [[0,10]], + ranges: [[0, 10]], }); expect(body.extensions).toBeTruthy(); - expect(body.extensions["custom_extension"]).toEqual({initial:false}); + expect(body.extensions["custom_extension"]).toEqual({ initial: false }); }).respond(200, function() { return { pos: "12", @@ -166,7 +166,7 @@ describe("SlidingSync", () => { }); expect(body.lists[0]).toEqual(listInfo); expect(body.extensions).toBeTruthy(); - expect(body.extensions["custom_extension"]).toEqual({initial:true}); + expect(body.extensions["custom_extension"]).toEqual({ initial: true }); }).respond(200, function() { return { pos: "1", From 3a120f8fb8eb346310a69fef69e94cb9c1c65dbe Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 19 Aug 2022 18:19:24 +0100 Subject: [PATCH 03/48] Assert pos values as well --- spec/integ/sliding-sync.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 90354ca38..3669514e1 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -118,6 +118,7 @@ describe("SlidingSync", () => { expect(body.lists[0]).toEqual(listInfo); expect(body.extensions).toBeTruthy(); expect(body.extensions["custom_extension"]).toEqual({ initial: true }); + expect(req.queryParams["pos"]).toBeUndefined(); txnId = body.txn_id; }).respond(200, function() { return { @@ -139,6 +140,7 @@ describe("SlidingSync", () => { }); expect(body.extensions).toBeTruthy(); expect(body.extensions["custom_extension"]).toEqual({ initial: false }); + expect(req.queryParams["pos"]).toEqual("11"); }).respond(200, function() { return { pos: "12", @@ -167,6 +169,7 @@ describe("SlidingSync", () => { expect(body.lists[0]).toEqual(listInfo); expect(body.extensions).toBeTruthy(); expect(body.extensions["custom_extension"]).toEqual({ initial: true }); + expect(req.queryParams["pos"]).toBeUndefined(); }).respond(200, function() { return { pos: "1", From f398e3564d14ba2445133b230ae2fed626dce0cb Mon Sep 17 00:00:00 2001 From: Faye Duxovni Date: Thu, 29 Sep 2022 16:25:19 -0400 Subject: [PATCH 04/48] Calculate IndexedDB versions automatically to reduce repeated information and possibilities for error (#2713) --- .../store/indexeddb-crypto-store-backend.ts | 63 +++++++++-------- src/store/indexeddb-local-backend.ts | 68 +++++++------------ 2 files changed, 57 insertions(+), 74 deletions(-) diff --git a/src/crypto/store/indexeddb-crypto-store-backend.ts b/src/crypto/store/indexeddb-crypto-store-backend.ts index 32539305d..51cd62a1e 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.ts +++ b/src/crypto/store/indexeddb-crypto-store-backend.ts @@ -34,7 +34,6 @@ import { IRoomEncryption } from "../RoomList"; import { InboundGroupSessionData } from "../OlmDevice"; import { IEncryptedPayload } from "../aes"; -export const VERSION = 11; const PROFILE_TRANSACTIONS = false; /** @@ -950,45 +949,34 @@ export class Backend implements CryptoStore { } } -export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void { - logger.log( - `Upgrading IndexedDBCryptoStore from version ${oldVersion}` - + ` to ${VERSION}`, - ); - if (oldVersion < 1) { // The database did not previously exist. - createDatabase(db); - } - if (oldVersion < 2) { - db.createObjectStore("account"); - } - if (oldVersion < 3) { +type DbMigration = (db: IDBDatabase) => void; +const DB_MIGRATIONS: DbMigration[] = [ + (db) => { createDatabase(db); }, + (db) => { db.createObjectStore("account"); }, + (db) => { const sessionsStore = db.createObjectStore("sessions", { keyPath: ["deviceKey", "sessionId"], }); sessionsStore.createIndex("deviceKey", "deviceKey"); - } - if (oldVersion < 4) { + }, + (db) => { db.createObjectStore("inbound_group_sessions", { keyPath: ["senderCurve25519Key", "sessionId"], }); - } - if (oldVersion < 5) { - db.createObjectStore("device_data"); - } - if (oldVersion < 6) { - db.createObjectStore("rooms"); - } - if (oldVersion < 7) { + }, + (db) => { db.createObjectStore("device_data"); }, + (db) => { db.createObjectStore("rooms"); }, + (db) => { db.createObjectStore("sessions_needing_backup", { keyPath: ["senderCurve25519Key", "sessionId"], }); - } - if (oldVersion < 8) { + }, + (db) => { db.createObjectStore("inbound_group_sessions_withheld", { keyPath: ["senderCurve25519Key", "sessionId"], }); - } - if (oldVersion < 9) { + }, + (db) => { const problemsStore = db.createObjectStore("session_problems", { keyPath: ["deviceKey", "time"], }); @@ -997,18 +985,29 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void { db.createObjectStore("notified_error_devices", { keyPath: ["userId", "deviceId"], }); - } - if (oldVersion < 10) { + }, + (db) => { db.createObjectStore("shared_history_inbound_group_sessions", { keyPath: ["roomId"], }); - } - if (oldVersion < 11) { + }, + (db) => { db.createObjectStore("parked_shared_history", { keyPath: ["roomId"], }); - } + }, // Expand as needed. +]; +export const VERSION = DB_MIGRATIONS.length; + +export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void { + logger.log( + `Upgrading IndexedDBCryptoStore from version ${oldVersion}` + + ` to ${VERSION}`, + ); + DB_MIGRATIONS.forEach((migration, index) => { + if (oldVersion <= index) migration(db); + }); } function createDatabase(db: IDBDatabase): void { diff --git a/src/store/indexeddb-local-backend.ts b/src/store/indexeddb-local-backend.ts index 7e4bd7673..0ab50db35 100644 --- a/src/store/indexeddb-local-backend.ts +++ b/src/store/indexeddb-local-backend.ts @@ -23,36 +23,31 @@ import { ISavedSync } from "./index"; import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend"; import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; -const VERSION = 4; +type DbMigration = (db: IDBDatabase) => void; +const DB_MIGRATIONS: DbMigration[] = [ + (db) => { + // Make user store, clobber based on user ID. (userId property of User objects) + db.createObjectStore("users", { keyPath: ["userId"] }); -function createDatabase(db: IDBDatabase): void { - // Make user store, clobber based on user ID. (userId property of User objects) - db.createObjectStore("users", { keyPath: ["userId"] }); + // Make account data store, clobber based on event type. + // (event.type property of MatrixEvent objects) + db.createObjectStore("accountData", { keyPath: ["type"] }); - // Make account data store, clobber based on event type. - // (event.type property of MatrixEvent objects) - db.createObjectStore("accountData", { keyPath: ["type"] }); - - // Make /sync store (sync tokens, room data, etc), always clobber (const key). - db.createObjectStore("sync", { keyPath: ["clobber"] }); -} - -function upgradeSchemaV2(db: IDBDatabase): void { - const oobMembersStore = db.createObjectStore( - "oob_membership_events", { - keyPath: ["room_id", "state_key"], - }); - oobMembersStore.createIndex("room", "room_id"); -} - -function upgradeSchemaV3(db: IDBDatabase): void { - db.createObjectStore("client_options", - { keyPath: ["clobber"] }); -} - -function upgradeSchemaV4(db: IDBDatabase): void { - db.createObjectStore("to_device_queue", { autoIncrement: true }); -} + // Make /sync store (sync tokens, room data, etc), always clobber (const key). + db.createObjectStore("sync", { keyPath: ["clobber"] }); + }, + (db) => { + const oobMembersStore = db.createObjectStore( + "oob_membership_events", { + keyPath: ["room_id", "state_key"], + }); + oobMembersStore.createIndex("room", "room_id"); + }, + (db) => { db.createObjectStore("client_options", { keyPath: ["clobber"] }); }, + (db) => { db.createObjectStore("to_device_queue", { autoIncrement: true }); }, + // Expand as needed. +]; +const VERSION = DB_MIGRATIONS.length; /** * Helper method to collect results from a Cursor and promiseify it. @@ -172,20 +167,9 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { logger.log( `LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`, ); - if (oldVersion < 1) { // The database did not previously exist. - this._isNewlyCreated = true; - createDatabase(db); - } - if (oldVersion < 2) { - upgradeSchemaV2(db); - } - if (oldVersion < 3) { - upgradeSchemaV3(db); - } - if (oldVersion < 4) { - upgradeSchemaV4(db); - } - // Expand as needed. + DB_MIGRATIONS.forEach((migration, index) => { + if (oldVersion <= index) migration(db); + }); }; req.onblocked = () => { From f3496633292f65066fa9c7c8c441c10e26f16fe4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Sep 2022 09:05:28 +0100 Subject: [PATCH 05/48] Add CI to protect against mixing `src` and `lib` imports (#2704) --- .eslintrc.js | 17 ++++ .github/workflows/static_analysis.yml | 10 +++ package.json | 3 +- scripts/switch_package_to_release.js | 17 ++++ src/@types/uia.ts | 2 +- src/crypto/algorithms/base.ts | 2 +- src/crypto/algorithms/olm.ts | 2 +- src/models/MSC3089Branch.ts | 2 +- src/store/indexeddb-backend.ts | 2 +- src/store/indexeddb-local-backend.ts | 2 +- src/store/indexeddb-remote-backend.ts | 2 +- src/utils.ts | 2 +- yarn.lock | 121 ++++++++++++++++++++++++-- 13 files changed, 170 insertions(+), 14 deletions(-) create mode 100755 scripts/switch_package_to_release.js diff --git a/.eslintrc.js b/.eslintrc.js index 5ed62980e..2baeaaac8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,14 +1,22 @@ module.exports = { plugins: [ "matrix-org", + "import", ], extends: [ "plugin:matrix-org/babel", + "plugin:import/typescript", ], env: { browser: true, node: true, }, + settings: { + "import/resolver": { + typescript: true, + node: true, + }, + }, // NOTE: These rules are frozen and new rules should not be added here. // New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/ rules: { @@ -36,6 +44,15 @@ module.exports = { // restrict EventEmitters to force callers to use TypedEventEmitter "no-restricted-imports": ["error", "events"], + + "import/no-restricted-paths": ["error", { + "zones": [{ + "target": "./src/", + "from": "./src/index.ts", + "message": "The package index is dynamic between src and lib depending on " + + "whether release or development, target the specific module or matrix.ts instead", + }], + }], }, overrides: [{ files: [ diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 10394ff79..44ccbf495 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -23,6 +23,16 @@ jobs: - name: Typecheck run: "yarn run lint:types" + - name: Switch js-sdk to release mode + run: | + scripts/switch_package_to_release.js + yarn install + yarn run build:compile + yarn run build:types + + - name: Typecheck (release mode) + run: "yarn run lint:types" + js_lint: name: "ESLint" runs-on: ubuntu-latest diff --git a/package.json b/package.json index bee650e2e..3ac584578 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,8 @@ "docdash": "^1.2.0", "eslint": "8.23.1", "eslint-config-google": "^0.14.0", - "eslint-plugin-import": "^2.25.4", + "eslint-import-resolver-typescript": "^3.5.1", + "eslint-plugin-import": "^2.26.0", "eslint-plugin-matrix-org": "^0.6.0", "exorcist": "^2.0.0", "fake-indexeddb": "^4.0.0", diff --git a/scripts/switch_package_to_release.js b/scripts/switch_package_to_release.js new file mode 100755 index 000000000..830c92dc4 --- /dev/null +++ b/scripts/switch_package_to_release.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +const fsProm = require('fs/promises'); + +const PKGJSON = 'package.json'; + +async function main() { + const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8')); + for (const field of ['main', 'typings']) { + if (pkgJson["matrix_lib_"+field] !== undefined) { + pkgJson[field] = pkgJson["matrix_lib_"+field]; + } + } + await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2)); +} + +main(); diff --git a/src/@types/uia.ts b/src/@types/uia.ts index 079306135..a976083a6 100644 --- a/src/@types/uia.ts +++ b/src/@types/uia.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IAuthData } from ".."; +import { IAuthData } from "../interactive-auth"; /** * Helper type to represent HTTP request body for a UIA enabled endpoint diff --git a/src/crypto/algorithms/base.ts b/src/crypto/algorithms/base.ts index 898a04dd8..070796720 100644 --- a/src/crypto/algorithms/base.ts +++ b/src/crypto/algorithms/base.ts @@ -23,7 +23,7 @@ limitations under the License. import { MatrixClient } from "../../client"; import { Room } from "../../models/room"; import { OlmDevice } from "../OlmDevice"; -import { MatrixEvent, RoomMember } from "../.."; +import { MatrixEvent, RoomMember } from "../../matrix"; import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from ".."; import { DeviceInfo } from "../deviceinfo"; import { IRoomEncryption } from "../RoomList"; diff --git a/src/crypto/algorithms/olm.ts b/src/crypto/algorithms/olm.ts index 38b1c97b3..14ee7516a 100644 --- a/src/crypto/algorithms/olm.ts +++ b/src/crypto/algorithms/olm.ts @@ -30,7 +30,7 @@ import { registerAlgorithm, } from "./base"; import { Room } from '../../models/room'; -import { MatrixEvent } from "../.."; +import { MatrixEvent } from "../../models/event"; import { IEventDecryptionResult } from "../index"; import { IInboundSession } from "../OlmDevice"; diff --git a/src/models/MSC3089Branch.ts b/src/models/MSC3089Branch.ts index 0c2082a2a..a230a4f1a 100644 --- a/src/models/MSC3089Branch.ts +++ b/src/models/MSC3089Branch.ts @@ -20,7 +20,7 @@ import { IContent, MatrixEvent } from "./event"; import { MSC3089TreeSpace } from "./MSC3089TreeSpace"; import { EventTimeline } from "./event-timeline"; import { FileType } from "../http-api"; -import type { ISendEventResponse } from ".."; +import type { ISendEventResponse } from "../@types/requests"; /** * Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference diff --git a/src/store/indexeddb-backend.ts b/src/store/indexeddb-backend.ts index 93d1cb3ab..3a14fed7d 100644 --- a/src/store/indexeddb-backend.ts +++ b/src/store/indexeddb-backend.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { ISavedSync } from "./index"; -import { IEvent, IStartClientOpts, IStateEventWithRoomId, ISyncResponse } from ".."; +import { IEvent, IStartClientOpts, IStateEventWithRoomId, ISyncResponse } from "../matrix"; import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; export interface IIndexedDBBackend { diff --git a/src/store/indexeddb-local-backend.ts b/src/store/indexeddb-local-backend.ts index 0ab50db35..32cdc95aa 100644 --- a/src/store/indexeddb-local-backend.ts +++ b/src/store/indexeddb-local-backend.ts @@ -18,7 +18,7 @@ import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../syn import * as utils from "../utils"; import * as IndexedDBHelpers from "../indexeddb-helpers"; import { logger } from '../logger'; -import { IStartClientOpts, IStateEventWithRoomId } from ".."; +import { IStartClientOpts, IStateEventWithRoomId } from "../matrix"; import { ISavedSync } from "./index"; import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend"; import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; diff --git a/src/store/indexeddb-remote-backend.ts b/src/store/indexeddb-remote-backend.ts index 67ab2ccd2..8be023f2b 100644 --- a/src/store/indexeddb-remote-backend.ts +++ b/src/store/indexeddb-remote-backend.ts @@ -18,7 +18,7 @@ import { logger } from "../logger"; import { defer, IDeferred } from "../utils"; import { ISavedSync } from "./index"; import { IStartClientOpts } from "../client"; -import { IStateEventWithRoomId, ISyncResponse } from ".."; +import { IStateEventWithRoomId, ISyncResponse } from "../matrix"; import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend"; import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; diff --git a/src/utils.ts b/src/utils.ts index f51be323b..2875cf3cf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -24,7 +24,7 @@ import unhomoglyph from "unhomoglyph"; import promiseRetry from "p-retry"; import type * as NodeCrypto from "crypto"; -import { MatrixEvent } from "."; +import { MatrixEvent } from "./models/event"; import { M_TIMESTAMP } from "./@types/location"; import { ReceiptType } from "./@types/read_receipts"; diff --git a/yarn.lock b/yarn.lock index 889233010..b6869e6fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1546,6 +1546,18 @@ dependencies: "@octokit/openapi-types" "^12.11.0" +"@pkgr/utils@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.3.1.tgz#0a9b06ffddee364d6642b3cd562ca76f55b34a03" + integrity sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw== + dependencies: + cross-spawn "^7.0.3" + is-glob "^4.0.3" + open "^8.4.0" + picocolors "^1.0.0" + tiny-glob "^0.2.9" + tslib "^2.4.0" + "@sinclair/typebox@^0.24.1": version "0.24.42" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.42.tgz#a74b608d494a1f4cc079738e050142a678813f52" @@ -2916,6 +2928,11 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" @@ -3081,6 +3098,14 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +enhanced-resolve@^5.10.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6" + integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + entities@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" @@ -3207,6 +3232,19 @@ eslint-import-resolver-node@^0.3.6: debug "^3.2.7" resolve "^1.20.0" +eslint-import-resolver-typescript@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.1.tgz#c72634da072eebd04fe73007fa58a62c333c8147" + integrity sha512-U7LUjNJPYjNsHvAUAkt/RU3fcTSpbllA0//35B4eLYTX74frmOepbt7F7J3D1IGtj9k21buOpaqtDd4ZlS/BYQ== + dependencies: + debug "^4.3.4" + enhanced-resolve "^5.10.0" + get-tsconfig "^4.2.0" + globby "^13.1.2" + is-core-module "^2.10.0" + is-glob "^4.0.3" + synckit "^0.8.3" + eslint-module-utils@^2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" @@ -3214,7 +3252,7 @@ eslint-module-utils@^2.7.3: dependencies: debug "^3.2.7" -eslint-plugin-import@^2.25.4: +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== @@ -3480,7 +3518,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.9: +fast-glob@^3.2.11, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== @@ -3703,6 +3741,11 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +get-tsconfig@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.2.0.tgz#ff368dd7104dab47bf923404eb93838245c66543" + integrity sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg== + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -3748,6 +3791,11 @@ globals@^13.15.0: dependencies: type-fest "^0.20.2" +globalyzer@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" + integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== + globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -3760,7 +3808,23 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -graceful-fs@^4.1.9, graceful-fs@^4.2.9: +globby@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515" + integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.2.11" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^4.0.0" + +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + +graceful-fs@^4.1.9, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -4015,7 +4079,7 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.4: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== -is-core-module@^2.8.1, is-core-module@^2.9.0: +is-core-module@^2.10.0, is-core-module@^2.8.1, is-core-module@^2.9.0: version "2.10.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== @@ -4029,6 +4093,11 @@ is-date-object@^1.0.1: dependencies: has-tostringtag "^1.0.0" +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-expression@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-3.0.0.tgz#39acaa6be7fd1f3471dc42c7416e61c24317ac9f" @@ -4162,6 +4231,13 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -5244,6 +5320,15 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" + integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -6075,6 +6160,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -6298,6 +6388,14 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +synckit@^0.8.3: + version "0.8.4" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.4.tgz#0e6b392b73fafdafcde56692e3352500261d64ec" + integrity sha512-Dn2ZkzMdSX827QbowGbU/4yjWuvNaCoScLLoMo/yKbu+P4GBR6cRGKZH27k6a9bRzdqcyd1DE96pQtQ6uNkmyw== + dependencies: + "@pkgr/utils" "^2.3.1" + tslib "^2.4.0" + syntax-error@^1.1.1: version "1.4.0" resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c" @@ -6310,6 +6408,11 @@ taffydb@2.6.2: resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" integrity sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA== +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -6375,6 +6478,14 @@ timers-ext@^0.1.7: es5-ext "~0.10.46" next-tick "1" +tiny-glob@^0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" + integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== + dependencies: + globalyzer "0.1.0" + globrex "^0.1.2" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -6464,7 +6575,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.1: +tslib@^2.0.1, tslib@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== From 9bb5afe5c014a643cf2ee9a0601b5ed5a73dd893 Mon Sep 17 00:00:00 2001 From: Stanislav Demydiuk Date: Fri, 30 Sep 2022 11:12:31 +0300 Subject: [PATCH 06/48] Fix issue in sync when crypto is not supported by client (#2715) Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- spec/integ/matrix-client-crypto.spec.ts | 8 ++--- spec/integ/matrix-client-syncing.spec.ts | 38 ++++++++++++++++++++++++ src/client.ts | 2 +- src/sync.ts | 29 ++++++++++-------- 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/spec/integ/matrix-client-crypto.spec.ts b/spec/integ/matrix-client-crypto.spec.ts index d0f46b9e5..262975ead 100644 --- a/spec/integ/matrix-client-crypto.spec.ts +++ b/spec/integ/matrix-client-crypto.spec.ts @@ -132,7 +132,7 @@ async function aliDownloadsKeys(): Promise { // check that the localStorage is updated as we expect (not sure this is // an integration test, but meh) await Promise.all([p1(), p2()]); - await aliTestClient.client.crypto.deviceList.saveIfDirty(); + await aliTestClient.client.crypto!.deviceList.saveIfDirty(); // @ts-ignore - protected aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { const devices = data.devices[bobUserId]; @@ -494,7 +494,7 @@ describe("MatrixClient crypto", () => { aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); await aliTestClient.start(); await bobTestClient.start(); - bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); await firstSync(aliTestClient); await aliEnablesEncryption(); await aliSendsFirstMessage(); @@ -505,7 +505,7 @@ describe("MatrixClient crypto", () => { aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); await aliTestClient.start(); await bobTestClient.start(); - bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); await firstSync(aliTestClient); await aliEnablesEncryption(); await aliSendsFirstMessage(); @@ -569,7 +569,7 @@ describe("MatrixClient crypto", () => { aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); await aliTestClient.start(); await bobTestClient.start(); - bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); await firstSync(aliTestClient); await aliEnablesEncryption(); await aliSendsFirstMessage(); diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index 03126c506..e07e52c7b 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -225,6 +225,44 @@ describe("MatrixClient syncing", () => { return httpBackend!.flushAllExpected(); }); + + it("should emit ClientEvent.Room when invited while crypto is disabled", async () => { + const roomId = "!invite:example.org"; + + // First sync: an invite + const inviteSyncRoomSection = { + invite: { + [roomId]: { + invite_state: { + events: [{ + type: "m.room.member", + state_key: selfUserId, + content: { + membership: "invite", + }, + }], + }, + }, + }, + }; + httpBackend!.when("GET", "/sync").respond(200, { + ...syncData, + rooms: inviteSyncRoomSection, + }); + + // First fire: an initial invite + let fires = 0; + client!.once(ClientEvent.Room, (room) => { + fires++; + expect(room.roomId).toBe(roomId); + }); + + // noinspection ES6MissingAwait + client!.startClient(); + await httpBackend!.flushAllExpected(); + + expect(fires).toBe(1); + }); }); describe("initial sync", () => { diff --git a/src/client.ts b/src/client.ts index 918dfccaa..e0e051126 100644 --- a/src/client.ts +++ b/src/client.ts @@ -923,7 +923,7 @@ export class MatrixClient extends TypedEventEmitter } = {}; public identityServer: IIdentityServerProvider; public http: MatrixHttpApi; // XXX: Intended private, used in code. - public crypto: Crypto; // XXX: Intended private, used in code. + public crypto?: Crypto; // XXX: Intended private, used in code. public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code. public callEventHandler: CallEventHandler; // XXX: Intended private, used in code. public supportsCallTransfer = false; // XXX: Intended private, used in code. diff --git a/src/sync.ts b/src/sync.ts index 7defdc4b1..768547949 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1200,19 +1200,22 @@ export class SyncApi { await this.processRoomEvents(room, stateEvents); const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId())?.getSender(); - const parkedHistory = await client.crypto.cryptoStore.takeParkedSharedHistory(room.roomId); - for (const parked of parkedHistory) { - if (parked.senderId === inviter) { - await this.client.crypto.olmDevice.addInboundGroupSession( - room.roomId, - parked.senderKey, - parked.forwardingCurve25519KeyChain, - parked.sessionId, - parked.sessionKey, - parked.keysClaimed, - true, - { sharedHistory: true, untrusted: true }, - ); + + if (client.isCryptoEnabled()) { + const parkedHistory = await client.crypto.cryptoStore.takeParkedSharedHistory(room.roomId); + for (const parked of parkedHistory) { + if (parked.senderId === inviter) { + await client.crypto.olmDevice.addInboundGroupSession( + room.roomId, + parked.senderKey, + parked.forwardingCurve25519KeyChain, + parked.sessionId, + parked.sessionKey, + parked.keysClaimed, + true, + { sharedHistory: true, untrusted: true }, + ); + } } } From 5afe373446068961389613974abf1f7246da08b0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Sep 2022 10:56:36 +0100 Subject: [PATCH 07/48] Fix release-npm.yml dist-tag npm token passing mechanism (#2717) --- .github/workflows/release-npm.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index c70408fb0..84d88ea5e 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -36,5 +36,6 @@ jobs: package=$(cat package.json | jq -er .name) npm dist-tag add "$package@$release" latest env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + # JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc + INPUT_TOKEN: ${{ secrets.NPM_TOKEN }} release: ${{ steps.npm-publish.outputs.version }} From 5520aa3e2a1e1f9ec426c37a25bac349a648c757 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Fri, 30 Sep 2022 10:57:04 +0100 Subject: [PATCH 08/48] [Backport staging] Fix issue in sync when crypto is not supported by client (#2716) Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Stanislav Demydiuk --- spec/integ/matrix-client-crypto.spec.ts | 8 ++--- spec/integ/matrix-client-syncing.spec.ts | 38 ++++++++++++++++++++++++ src/client.ts | 2 +- src/sync.ts | 29 ++++++++++-------- 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/spec/integ/matrix-client-crypto.spec.ts b/spec/integ/matrix-client-crypto.spec.ts index d0f46b9e5..262975ead 100644 --- a/spec/integ/matrix-client-crypto.spec.ts +++ b/spec/integ/matrix-client-crypto.spec.ts @@ -132,7 +132,7 @@ async function aliDownloadsKeys(): Promise { // check that the localStorage is updated as we expect (not sure this is // an integration test, but meh) await Promise.all([p1(), p2()]); - await aliTestClient.client.crypto.deviceList.saveIfDirty(); + await aliTestClient.client.crypto!.deviceList.saveIfDirty(); // @ts-ignore - protected aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { const devices = data.devices[bobUserId]; @@ -494,7 +494,7 @@ describe("MatrixClient crypto", () => { aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); await aliTestClient.start(); await bobTestClient.start(); - bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); await firstSync(aliTestClient); await aliEnablesEncryption(); await aliSendsFirstMessage(); @@ -505,7 +505,7 @@ describe("MatrixClient crypto", () => { aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); await aliTestClient.start(); await bobTestClient.start(); - bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); await firstSync(aliTestClient); await aliEnablesEncryption(); await aliSendsFirstMessage(); @@ -569,7 +569,7 @@ describe("MatrixClient crypto", () => { aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); await aliTestClient.start(); await bobTestClient.start(); - bobTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); await firstSync(aliTestClient); await aliEnablesEncryption(); await aliSendsFirstMessage(); diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index 03126c506..e07e52c7b 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -225,6 +225,44 @@ describe("MatrixClient syncing", () => { return httpBackend!.flushAllExpected(); }); + + it("should emit ClientEvent.Room when invited while crypto is disabled", async () => { + const roomId = "!invite:example.org"; + + // First sync: an invite + const inviteSyncRoomSection = { + invite: { + [roomId]: { + invite_state: { + events: [{ + type: "m.room.member", + state_key: selfUserId, + content: { + membership: "invite", + }, + }], + }, + }, + }, + }; + httpBackend!.when("GET", "/sync").respond(200, { + ...syncData, + rooms: inviteSyncRoomSection, + }); + + // First fire: an initial invite + let fires = 0; + client!.once(ClientEvent.Room, (room) => { + fires++; + expect(room.roomId).toBe(roomId); + }); + + // noinspection ES6MissingAwait + client!.startClient(); + await httpBackend!.flushAllExpected(); + + expect(fires).toBe(1); + }); }); describe("initial sync", () => { diff --git a/src/client.ts b/src/client.ts index 07fbaecec..beae5aed2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -905,7 +905,7 @@ export class MatrixClient extends TypedEventEmitter } = {}; public identityServer: IIdentityServerProvider; public http: MatrixHttpApi; // XXX: Intended private, used in code. - public crypto: Crypto; // XXX: Intended private, used in code. + public crypto?: Crypto; // XXX: Intended private, used in code. public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code. public callEventHandler: CallEventHandler; // XXX: Intended private, used in code. public supportsCallTransfer = false; // XXX: Intended private, used in code. diff --git a/src/sync.ts b/src/sync.ts index 72a8e7f82..640dcfd6e 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1200,19 +1200,22 @@ export class SyncApi { await this.processRoomEvents(room, stateEvents); const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId())?.getSender(); - const parkedHistory = await client.crypto.cryptoStore.takeParkedSharedHistory(room.roomId); - for (const parked of parkedHistory) { - if (parked.senderId === inviter) { - await this.client.crypto.olmDevice.addInboundGroupSession( - room.roomId, - parked.senderKey, - parked.forwardingCurve25519KeyChain, - parked.sessionId, - parked.sessionKey, - parked.keysClaimed, - true, - { sharedHistory: true, untrusted: true }, - ); + + if (client.isCryptoEnabled()) { + const parkedHistory = await client.crypto.cryptoStore.takeParkedSharedHistory(room.roomId); + for (const parked of parkedHistory) { + if (parked.senderId === inviter) { + await client.crypto.olmDevice.addInboundGroupSession( + room.roomId, + parked.senderKey, + parked.forwardingCurve25519KeyChain, + parked.sessionId, + parked.sessionKey, + parked.keysClaimed, + true, + { sharedHistory: true, untrusted: true }, + ); + } } } From 3efc18cfde098d9ab50f4df9e00d2f369895a5b1 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 30 Sep 2022 10:58:33 +0100 Subject: [PATCH 09/48] Prepare changelog for v20.0.2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eed6a209..9071a7ae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [20.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.2) (2022-09-30) +================================================================================================== + +## 🐛 Bug Fixes + * Fix issue in sync when crypto is not supported by client ([\#2715](https://github.com/matrix-org/matrix-js-sdk/pull/2715)). Contributed by @stas-demydiuk. + Changes in [20.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.1) (2022-09-28) ================================================================================================== From 6166a8f7fd6fd13c398122fba844d31ca73a2238 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 30 Sep 2022 10:58:34 +0100 Subject: [PATCH 10/48] v20.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4316f65af..3e680eb67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "20.0.1", + "version": "20.0.2", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=12.9.0" From 887e15aac5689ff877f261fac7e210dc44b291c4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Sep 2022 11:41:04 +0100 Subject: [PATCH 11/48] Fix IDB initial migration handling causing spurious lazy loading upgrade loops (#2718) --- spec/unit/stores/indexeddb.spec.ts | 54 ++++++++++++++++++++++++++++ src/store/indexeddb-local-backend.ts | 4 +++ 2 files changed, 58 insertions(+) diff --git a/spec/unit/stores/indexeddb.spec.ts b/spec/unit/stores/indexeddb.spec.ts index 3fc7477cc..d0dd87243 100644 --- a/spec/unit/stores/indexeddb.spec.ts +++ b/spec/unit/stores/indexeddb.spec.ts @@ -20,6 +20,7 @@ import 'jest-localstorage-mock'; import { IndexedDBStore, IStateEventWithRoomId, MemoryStore } from "../../../src"; import { emitPromise } from "../../test-utils/test-utils"; import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend"; +import { defer } from "../../../src/utils"; describe("IndexedDBStore", () => { afterEach(() => { @@ -111,4 +112,57 @@ describe("IndexedDBStore", () => { await store.setPendingEvents(roomId, []); expect(localStorage.getItem("mx_pending_events_" + roomId)).toBeNull(); }); + + it("should resolve isNewlyCreated to true if no database existed initially", async () => { + const store = new IndexedDBStore({ + indexedDB, + dbName: "db1", + localStorage, + }); + await store.startup(); + + await expect(store.isNewlyCreated()).resolves.toBeTruthy(); + }); + + it("should resolve isNewlyCreated to false if database existed already", async () => { + let store = new IndexedDBStore({ + indexedDB, + dbName: "db2", + localStorage, + }); + await store.startup(); + + store = new IndexedDBStore({ + indexedDB, + dbName: "db2", + localStorage, + }); + await store.startup(); + + await expect(store.isNewlyCreated()).resolves.toBeFalsy(); + }); + + it("should resolve isNewlyCreated to false if database existed already but needs upgrade", async () => { + const deferred = defer(); + // seed db3 to Version 1 so it forces a migration + const req = indexedDB.open("matrix-js-sdk:db3", 1); + req.onupgradeneeded = () => { + const db = req.result; + db.createObjectStore("users", { keyPath: ["userId"] }); + db.createObjectStore("accountData", { keyPath: ["type"] }); + db.createObjectStore("sync", { keyPath: ["clobber"] }); + }; + req.onsuccess = deferred.resolve; + await deferred.promise; + req.result.close(); + + const store = new IndexedDBStore({ + indexedDB, + dbName: "db3", + localStorage, + }); + await store.startup(); + + await expect(store.isNewlyCreated()).resolves.toBeFalsy(); + }); }); diff --git a/src/store/indexeddb-local-backend.ts b/src/store/indexeddb-local-backend.ts index 32cdc95aa..908ecec9e 100644 --- a/src/store/indexeddb-local-backend.ts +++ b/src/store/indexeddb-local-backend.ts @@ -167,6 +167,10 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { logger.log( `LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`, ); + if (oldVersion < 1) { + // The database did not previously exist + this._isNewlyCreated = true; + } DB_MIGRATIONS.forEach((migration, index) => { if (oldVersion <= index) migration(db); }); From 4b283015badec7431764e5671566285c7b5a0e00 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 08:30:28 +0100 Subject: [PATCH 12/48] Lock file maintenance (#2721) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 1017 ++++++++++++++++++++++++++--------------------------- 1 file changed, 489 insertions(+), 528 deletions(-) diff --git a/yarn.lock b/yarn.lock index b6869e6fb..8965240fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,17 +3,17 @@ "@actions/core@^1.4.0": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.9.1.tgz#97c0201b1f9856df4f7c3a375cdcdb0c2a2f750b" - integrity sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA== + version "1.10.0" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.10.0.tgz#44551c3c71163949a2f06e94d9ca2157a0cfac4f" + integrity sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug== dependencies: "@actions/http-client" "^2.0.1" uuid "^8.3.2" "@actions/github@^5.0.0": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.0.3.tgz#b305765d6173962d113451ea324ff675aa674f35" - integrity sha512-myjA/pdLQfhUGLtRZC/J4L1RXOG4o6aYdiEq+zr5wVVKljzbFld+xv10k1FX6IkIJtNxbAq44BdwSNpQ015P0A== + version "5.1.1" + resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.1.1.tgz#40b9b9e1323a5efcf4ff7dadd33d8ea51651bbcb" + integrity sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g== dependencies: "@actions/http-client" "^2.0.1" "@octokit/core" "^3.6.0" @@ -36,9 +36,9 @@ "@jridgewell/trace-mapping" "^0.3.9" "@babel/cli@^7.12.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.18.10.tgz#4211adfc45ffa7d4f3cee6b60bb92e9fe68fe56a" - integrity sha512-dLvWH+ZDFAkd2jPBSghrsFBuXrREvFwjpDycXbmUoeochqKYe4zNSLEJYErpLg8dvxvZYe79/MkN461XCwpnGw== + version "7.19.3" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.19.3.tgz#55914ed388e658e0b924b3a95da1296267e278e2" + integrity sha512-643/TybmaCAe101m2tSVHi9UKpETXP9c/Ff4mD2tAwkdP6esKIfaauZFc67vGEM6r9fekbEGid+sZhbEnSe3dg== dependencies: "@jridgewell/trace-mapping" "^0.3.8" commander "^4.0.1" @@ -58,26 +58,26 @@ dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8", "@babel/compat-data@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.1.tgz#72d647b4ff6a4f82878d184613353af1dd0290f9" - integrity sha512-72a9ghR0gnESIa7jBN53U32FOVCEoztyIlKaNoU05zRhEecduGK9L9c3ww7Mp06JiR+0ls0GBPFJQwwtjn9ksg== +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8", "@babel/compat-data@^7.19.3": + version "7.19.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.3.tgz#707b939793f867f5a73b2666e6d9a3396eb03151" + integrity sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw== "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.5": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.19.1.tgz#c8fa615c5e88e272564ace3d42fbc8b17bfeb22b" - integrity sha512-1H8VgqXme4UXCRv7/Wa1bq7RVymKOzC7znjyFM8KiEzwFqcKUKYNoQef4GhdklgNvoBXyW4gYhuBNCM5o1zImw== + version "7.19.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.19.3.tgz#2519f62a51458f43b682d61583c3810e7dcee64c" + integrity sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.19.0" - "@babel/helper-compilation-targets" "^7.19.1" + "@babel/generator" "^7.19.3" + "@babel/helper-compilation-targets" "^7.19.3" "@babel/helper-module-transforms" "^7.19.0" "@babel/helpers" "^7.19.0" - "@babel/parser" "^7.19.1" + "@babel/parser" "^7.19.3" "@babel/template" "^7.18.10" - "@babel/traverse" "^7.19.1" - "@babel/types" "^7.19.0" + "@babel/traverse" "^7.19.3" + "@babel/types" "^7.19.3" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -100,21 +100,12 @@ dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.12.11": - version "7.18.13" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.13.tgz#59550cbb9ae79b8def15587bdfbaa388c4abf212" - integrity sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ== +"@babel/generator@^7.12.11", "@babel/generator@^7.19.3", "@babel/generator@^7.7.2": + version "7.19.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.3.tgz#d7f4d1300485b4547cb6f94b27d10d237b42bf59" + integrity sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ== dependencies: - "@babel/types" "^7.18.13" - "@jridgewell/gen-mapping" "^0.3.2" - jsesc "^2.5.1" - -"@babel/generator@^7.18.13", "@babel/generator@^7.19.0", "@babel/generator@^7.7.2": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.0.tgz#785596c06425e59334df2ccee63ab166b738419a" - integrity sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg== - dependencies: - "@babel/types" "^7.19.0" + "@babel/types" "^7.19.3" "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" @@ -133,17 +124,17 @@ "@babel/helper-explode-assignable-expression" "^7.18.6" "@babel/types" "^7.18.9" -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.19.0", "@babel/helper-compilation-targets@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.1.tgz#7f630911d83b408b76fe584831c98e5395d7a17c" - integrity sha512-LlLkkqhCMyz2lkQPvJNdIYU7O5YjWRgC2R4omjCTpZd8u8KMQzZvX4qce+/BluN1rcQiV7BoGUpmQ0LeHerbhg== +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.19.0", "@babel/helper-compilation-targets@^7.19.3": + version "7.19.3" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz#a10a04588125675d7c7ae299af86fa1b2ee038ca" + integrity sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg== dependencies: - "@babel/compat-data" "^7.19.1" + "@babel/compat-data" "^7.19.3" "@babel/helper-validator-option" "^7.18.6" browserslist "^4.21.3" semver "^6.3.0" -"@babel/helper-create-class-features-plugin@^7.18.6": +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.19.0": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.19.0.tgz#bfd6904620df4e46470bae4850d66be1054c404b" integrity sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw== @@ -156,19 +147,6 @@ "@babel/helper-replace-supers" "^7.18.9" "@babel/helper-split-export-declaration" "^7.18.6" -"@babel/helper-create-class-features-plugin@^7.18.9": - version "7.18.13" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.13.tgz#63e771187bd06d234f95fdf8bd5f8b6429de6298" - integrity sha512-hDvXp+QYxSRL+23mpAlSGxHMDyIGChm0/AwTfTAAK5Ufe40nCsyNdaYCGuK91phn/fVu9kqayImRDkvNAgdrsA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.18.9" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.18.9" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.19.0": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz#7976aca61c0984202baca73d84e2337a5424a41b" @@ -303,7 +281,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== -"@babel/helper-validator-identifier@^7.18.6": +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== @@ -341,15 +319,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.13", "@babel/parser@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.1.tgz#6f6d6c2e621aad19a92544cc217ed13f1aac5b4c" - integrity sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A== - -"@babel/parser@^7.2.3", "@babel/parser@^7.9.4": - version "7.18.13" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4" - integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.19.3", "@babel/parser@^7.2.3", "@babel/parser@^7.9.4": + version "7.19.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.3.tgz#8dd36d17c53ff347f9e55c328710321b49479a9a" + integrity sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -880,12 +853,12 @@ "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-transform-typescript@^7.18.6": - version "7.18.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.12.tgz#712e9a71b9e00fde9f8c0238e0cceee86ab2f8fd" - integrity sha512-2vjjam0cum0miPkenUbQswKowuxs/NjMwIKEq0zwegRxXk12C9YOF9STXnaUptITOtOJHKHpzvvWYOjbm6tc0w== + version "7.19.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.19.3.tgz#4f1db1e0fe278b42ddbc19ec2f6cd2f8262e35d6" + integrity sha512-z6fnuK9ve9u/0X0rRvI9MY0xg+DOUaABDYOe+/SQTxtlptaBB/V9JIUxJn6xp3lMBeb9qe8xSFmHU35oZDXD+w== dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-create-class-features-plugin" "^7.19.0" + "@babel/helper-plugin-utils" "^7.19.0" "@babel/plugin-syntax-typescript" "^7.18.6" "@babel/plugin-transform-unicode-escapes@^7.18.10": @@ -904,12 +877,12 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/preset-env@^7.12.11": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.19.1.tgz#9f04c916f9c0205a48ebe5cc1be7768eb1983f67" - integrity sha512-c8B2c6D16Lp+Nt6HcD+nHl0VbPKVnNPTpszahuxJJnurfMtKeZ80A+qUv48Y7wqvS+dTFuLuaM9oYxyNHbCLWA== + version "7.19.3" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.19.3.tgz#52cd19abaecb3f176a4ff9cc5e15b7bf06bec754" + integrity sha512-ziye1OTc9dGFOAXSWKUqQblYHNlBOaDl8wzqf2iKXJAltYiR3hKHUKmkt+S9PppW7RQpq4fFCrwwpIDj/f5P4w== dependencies: - "@babel/compat-data" "^7.19.1" - "@babel/helper-compilation-targets" "^7.19.1" + "@babel/compat-data" "^7.19.3" + "@babel/helper-compilation-targets" "^7.19.3" "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-validator-option" "^7.18.6" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" @@ -977,7 +950,7 @@ "@babel/plugin-transform-unicode-escapes" "^7.18.10" "@babel/plugin-transform-unicode-regex" "^7.18.6" "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.19.0" + "@babel/types" "^7.19.3" babel-plugin-polyfill-corejs2 "^0.3.3" babel-plugin-polyfill-corejs3 "^0.6.0" babel-plugin-polyfill-regenerator "^0.4.1" @@ -1031,54 +1004,29 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@^7.1.6": - version "7.18.13" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.13.tgz#5ab59ef51a997b3f10c4587d648b9696b6cb1a68" - integrity sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA== +"@babel/traverse@^7.1.6", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.19.3", "@babel/traverse@^7.7.2": + version "7.19.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.3.tgz#3a3c5348d4988ba60884e8494b0592b2f15a04b4" + integrity sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.18.13" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.18.13" - "@babel/types" "^7.18.13" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.7.2": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.1.tgz#0fafe100a8c2a603b4718b1d9bf2568d1d193347" - integrity sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.19.0" + "@babel/generator" "^7.19.3" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.19.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.19.1" - "@babel/types" "^7.19.0" + "@babel/parser" "^7.19.3" + "@babel/types" "^7.19.3" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.13", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.0.tgz#75f21d73d73dc0351f3368d28db73465f4814600" - integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA== +"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.19.3", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.19.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.3.tgz#fc420e6bbe54880bce6779ffaf315f5e43ec9624" + integrity sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw== dependencies: "@babel/helper-string-parser" "^7.18.10" - "@babel/helper-validator-identifier" "^7.18.6" - to-fast-properties "^2.0.0" - -"@babel/types@^7.2.0": - version "7.18.13" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a" - integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ== - dependencies: - "@babel/helper-string-parser" "^7.18.10" - "@babel/helper-validator-identifier" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" "@bcoe/v8-coverage@^0.2.3": @@ -1102,9 +1050,9 @@ strip-json-comments "^3.1.1" "@humanwhocodes/config-array@^0.10.4": - version "0.10.4" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.4.tgz#01e7366e57d2ad104feea63e72248f22015c520c" - integrity sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw== + version "0.10.7" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.7.tgz#6d53769fd0c222767e6452e8ebda825c22e9f0dc" + integrity sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" @@ -1141,28 +1089,28 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.0.3.tgz#a222ab87e399317a89db88a58eaec289519e807a" - integrity sha512-cGg0r+klVHSYnfE977S9wmpuQ9L+iYuYgL+5bPXiUlUynLLYunRxswEmhBzvrSKGof5AKiHuTTmUKAqRcDY9dg== +"@jest/console@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.1.2.tgz#0ae975a70004696f8320490fcaa1a4152f7b62e4" + integrity sha512-ujEBCcYs82BTmRxqfHMQggSlkUZP63AE5YEaTPj7eFyJOzukkTorstOUC7L6nE3w5SYadGVAnTsQ/ZjTGL0qYQ== dependencies: - "@jest/types" "^29.0.3" + "@jest/types" "^29.1.2" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^29.0.3" - jest-util "^29.0.3" + jest-message-util "^29.1.2" + jest-util "^29.1.2" slash "^3.0.0" -"@jest/core@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.0.3.tgz#ba22a9cbd0c7ba36e04292e2093c547bf53ec1fd" - integrity sha512-1d0hLbOrM1qQE3eP3DtakeMbKTcXiXP3afWxqz103xPyddS2NhnNghS7MaXx1dcDt4/6p4nlhmeILo2ofgi8cQ== +"@jest/core@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.1.2.tgz#e5ce7a71e7da45156a96fb5eeed11d18b67bd112" + integrity sha512-sCO2Va1gikvQU2ynDN8V4+6wB7iVrD2CvT0zaRst4rglf56yLly0NQ9nuRRAWFeimRf+tCdFsb1Vk1N9LrrMPA== dependencies: - "@jest/console" "^29.0.3" - "@jest/reporters" "^29.0.3" - "@jest/test-result" "^29.0.3" - "@jest/transform" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/console" "^29.1.2" + "@jest/reporters" "^29.1.2" + "@jest/test-result" "^29.1.2" + "@jest/transform" "^29.1.2" + "@jest/types" "^29.1.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" @@ -1170,32 +1118,32 @@ exit "^0.1.2" graceful-fs "^4.2.9" jest-changed-files "^29.0.0" - jest-config "^29.0.3" - jest-haste-map "^29.0.3" - jest-message-util "^29.0.3" + jest-config "^29.1.2" + jest-haste-map "^29.1.2" + jest-message-util "^29.1.2" jest-regex-util "^29.0.0" - jest-resolve "^29.0.3" - jest-resolve-dependencies "^29.0.3" - jest-runner "^29.0.3" - jest-runtime "^29.0.3" - jest-snapshot "^29.0.3" - jest-util "^29.0.3" - jest-validate "^29.0.3" - jest-watcher "^29.0.3" + jest-resolve "^29.1.2" + jest-resolve-dependencies "^29.1.2" + jest-runner "^29.1.2" + jest-runtime "^29.1.2" + jest-snapshot "^29.1.2" + jest-util "^29.1.2" + jest-validate "^29.1.2" + jest-watcher "^29.1.2" micromatch "^4.0.4" - pretty-format "^29.0.3" + pretty-format "^29.1.2" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.0.3.tgz#7745ec30a954e828e8cc6df6a13280d3b51d8f35" - integrity sha512-iKl272NKxYNQNqXMQandAIwjhQaGw5uJfGXduu8dS9llHi8jV2ChWrtOAVPnMbaaoDhnI3wgUGNDvZgHeEJQCA== +"@jest/environment@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.1.2.tgz#bb51a43fce9f960ba9a48f0b5b556f30618ebc0a" + integrity sha512-rG7xZ2UeOfvOVzoLIJ0ZmvPl4tBEQ2n73CZJSlzUjPw4or1oSWC0s0Rk0ZX+pIBJ04aVr6hLWFn1DFtrnf8MhQ== dependencies: - "@jest/fake-timers" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/fake-timers" "^29.1.2" + "@jest/types" "^29.1.2" "@types/node" "*" - jest-mock "^29.0.3" + jest-mock "^29.1.2" "@jest/expect-utils@^28.1.3": version "28.1.3" @@ -1204,53 +1152,53 @@ dependencies: jest-get-type "^28.0.2" -"@jest/expect-utils@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.0.3.tgz#f5bb86f5565bf2dacfca31ccbd887684936045b2" - integrity sha512-i1xUkau7K/63MpdwiRqaxgZOjxYs4f0WMTGJnYwUKubsNRZSeQbLorS7+I4uXVF9KQ5r61BUPAUMZ7Lf66l64Q== +"@jest/expect-utils@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.1.2.tgz#66dbb514d38f7d21456bc774419c9ae5cca3f88d" + integrity sha512-4a48bhKfGj/KAH39u0ppzNTABXQ8QPccWAFUFobWBaEMSMp+sB31Z2fK/l47c4a/Mu1po2ffmfAIPxXbVTXdtg== dependencies: jest-get-type "^29.0.0" -"@jest/expect@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.0.3.tgz#9dc7c46354eeb7a348d73881fba6402f5fdb2c30" - integrity sha512-6W7K+fsI23FQ01H/BWccPyDZFrnU9QlzDcKOjrNVU5L8yUORFAJJIpmyxWPW70+X624KUNqzZwPThPMX28aXEQ== +"@jest/expect@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.1.2.tgz#334a86395f621f1ab63ad95b06a588b9114d7b7a" + integrity sha512-FXw/UmaZsyfRyvZw3M6POgSNqwmuOXJuzdNiMWW9LCYo0GRoRDhg+R5iq5higmRTHQY7hx32+j7WHwinRmoILQ== dependencies: - expect "^29.0.3" - jest-snapshot "^29.0.3" + expect "^29.1.2" + jest-snapshot "^29.1.2" -"@jest/fake-timers@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.0.3.tgz#ad5432639b715d45a86a75c47fd75019bc36b22c" - integrity sha512-tmbUIo03x0TdtcZCESQ0oQSakPCpo7+s6+9mU19dd71MptkP4zCwoeZqna23//pgbhtT1Wq02VmA9Z9cNtvtCQ== +"@jest/fake-timers@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.1.2.tgz#f157cdf23b4da48ce46cb00fea28ed1b57fc271a" + integrity sha512-GppaEqS+QQYegedxVMpCe2xCXxxeYwQ7RsNx55zc8f+1q1qevkZGKequfTASI7ejmg9WwI+SJCrHe9X11bLL9Q== dependencies: - "@jest/types" "^29.0.3" + "@jest/types" "^29.1.2" "@sinonjs/fake-timers" "^9.1.2" "@types/node" "*" - jest-message-util "^29.0.3" - jest-mock "^29.0.3" - jest-util "^29.0.3" + jest-message-util "^29.1.2" + jest-mock "^29.1.2" + jest-util "^29.1.2" -"@jest/globals@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.0.3.tgz#681950c430fdc13ff9aa89b2d8d572ac0e4a1bf5" - integrity sha512-YqGHT65rFY2siPIHHFjuCGUsbzRjdqkwbat+Of6DmYRg5shIXXrLdZoVE/+TJ9O1dsKsFmYhU58JvIbZRU1Z9w== +"@jest/globals@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.1.2.tgz#826ede84bc280ae7f789cb72d325c48cd048b9d3" + integrity sha512-uMgfERpJYoQmykAd0ffyMq8wignN4SvLUG6orJQRe9WAlTRc9cdpCaE/29qurXixYJVZWUqIBXhSk8v5xN1V9g== dependencies: - "@jest/environment" "^29.0.3" - "@jest/expect" "^29.0.3" - "@jest/types" "^29.0.3" - jest-mock "^29.0.3" + "@jest/environment" "^29.1.2" + "@jest/expect" "^29.1.2" + "@jest/types" "^29.1.2" + jest-mock "^29.1.2" -"@jest/reporters@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.0.3.tgz#735f110e08b44b38729d8dbbb74063bdf5aba8a5" - integrity sha512-3+QU3d4aiyOWfmk1obDerie4XNCaD5Xo1IlKNde2yGEi02WQD+ZQD0i5Hgqm1e73sMV7kw6pMlCnprtEwEVwxw== +"@jest/reporters@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.1.2.tgz#5520898ed0a4ecf69d8b671e1dc8465d0acdfa6e" + integrity sha512-X4fiwwyxy9mnfpxL0g9DD0KcTmEIqP0jUdnc2cfa9riHy+I6Gwwp5vOZiwyg0vZxfSDxrOlK9S4+340W4d+DAA== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.0.3" - "@jest/test-result" "^29.0.3" - "@jest/transform" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/console" "^29.1.2" + "@jest/test-result" "^29.1.2" + "@jest/transform" "^29.1.2" + "@jest/types" "^29.1.2" "@jridgewell/trace-mapping" "^0.3.15" "@types/node" "*" chalk "^4.0.0" @@ -1263,9 +1211,9 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.1.3" - jest-message-util "^29.0.3" - jest-util "^29.0.3" - jest-worker "^29.0.3" + jest-message-util "^29.1.2" + jest-util "^29.1.2" + jest-worker "^29.1.2" slash "^3.0.0" string-length "^4.0.1" strip-ansi "^6.0.0" @@ -1295,42 +1243,42 @@ callsites "^3.0.0" graceful-fs "^4.2.9" -"@jest/test-result@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.0.3.tgz#b03d8ef4c58be84cd5d5d3b24d4b4c8cabbf2746" - integrity sha512-vViVnQjCgTmbhDKEonKJPtcFe9G/CJO4/Np4XwYJah+lF2oI7KKeRp8t1dFvv44wN2NdbDb/qC6pi++Vpp0Dlg== +"@jest/test-result@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.1.2.tgz#6a8d006eb2b31ce0287d1fc10d12b8ff8504f3c8" + integrity sha512-jjYYjjumCJjH9hHCoMhA8PCl1OxNeGgAoZ7yuGYILRJX9NjgzTN0pCT5qAoYR4jfOP8htIByvAlz9vfNSSBoVg== dependencies: - "@jest/console" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/console" "^29.1.2" + "@jest/types" "^29.1.2" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.0.3.tgz#0681061ad21fb8e293b49c4fdf7e631ca79240ba" - integrity sha512-Hf4+xYSWZdxTNnhDykr8JBs0yBN/nxOXyUQWfotBUqqy0LF9vzcFB0jm/EDNZCx587znLWTIgxcokW7WeZMobQ== +"@jest/test-sequencer@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.1.2.tgz#10bfd89c08bfdba382eb05cc79c1d23a01238a93" + integrity sha512-fU6dsUqqm8sA+cd85BmeF7Gu9DsXVWFdGn9taxM6xN1cKdcP/ivSgXh5QucFRFz1oZxKv3/9DYYbq0ULly3P/Q== dependencies: - "@jest/test-result" "^29.0.3" + "@jest/test-result" "^29.1.2" graceful-fs "^4.2.9" - jest-haste-map "^29.0.3" + jest-haste-map "^29.1.2" slash "^3.0.0" -"@jest/transform@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.0.3.tgz#9eb1fed2072a0354f190569807d1250572fb0970" - integrity sha512-C5ihFTRYaGDbi/xbRQRdbo5ddGtI4VSpmL6AIcZxdhwLbXMa7PcXxxqyI91vGOFHnn5aVM3WYnYKCHEqmLVGzg== +"@jest/transform@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.1.2.tgz#20f814696e04f090421f6d505c14bbfe0157062a" + integrity sha512-2uaUuVHTitmkx1tHF+eBjb4p7UuzBG7SXIaA/hNIkaMP6K+gXYGxP38ZcrofzqN0HeZ7A90oqsOa97WU7WZkSw== dependencies: "@babel/core" "^7.11.6" - "@jest/types" "^29.0.3" + "@jest/types" "^29.1.2" "@jridgewell/trace-mapping" "^0.3.15" babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.1.0" graceful-fs "^4.2.9" - jest-haste-map "^29.0.3" + jest-haste-map "^29.1.2" jest-regex-util "^29.0.0" - jest-util "^29.0.3" + jest-util "^29.1.2" micromatch "^4.0.4" pirates "^4.0.4" slash "^3.0.0" @@ -1348,10 +1296,10 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jest/types@^29.0.3": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.0.3.tgz#0be78fdddb1a35aeb2041074e55b860561c8ef63" - integrity sha512-coBJmOQvurXjN1Hh5PzF7cmsod0zLIOXpP8KD161mqNlroMhLcwpODiEzi7ZsRl5Z/AIuxpeNm8DCl43F4kz8A== +"@jest/types@^29.1.2": + version "29.1.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.1.2.tgz#7442d32b16bcd7592d9614173078b8c334ec730a" + integrity sha512-DcXGtoTykQB5jiwCmVr8H4vdg2OJhQex3qPkG+ISyDO7xQXbt/4R6dowcRyPemRnkH7JoHvZuxPBdlq+9JxFCg== dependencies: "@jest/schemas" "^29.0.0" "@types/istanbul-lib-coverage" "^2.0.0" @@ -1410,6 +1358,7 @@ "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz": version "3.2.12" + uid "0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz#0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9" "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": @@ -1559,9 +1508,9 @@ tslib "^2.4.0" "@sinclair/typebox@^0.24.1": - version "0.24.42" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.42.tgz#a74b608d494a1f4cc079738e050142a678813f52" - integrity sha512-d+2AtrHGyWek2u2ITF0lHRIv6Tt7X0dEHW+0rP+5aDCEjC3fiN2RBjrLD0yU0at52BcZbRGxLbAtXiR0hFCjYw== + version "0.24.44" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.44.tgz#0a0aa3bf4a155a678418527342a3ee84bd8caa5c" + integrity sha512-ka0W0KN5i6LfrSocduwliMMpqVgohtPFidKdMEOUjoOFCHcOOYkKsPRxfs5f15oPNHTm6ERAm0GV/+/LTKeiWg== "@sinonjs/commons@^1.7.0": version "1.8.3" @@ -1609,9 +1558,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.18.1" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.1.tgz#ce5e2c8c272b99b7a9fd69fa39f0b4cd85028bd9" - integrity sha512-FSdLaZh2UxaMuLp9lixWaHq/golWTRWOnRsAXzDTDSDOQLuZb1nsdCt6pJSPWSEQt2eFZ2YVk3oYhn+1kLMeMA== + version "7.18.2" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.2.tgz#235bf339d17185bdec25e024ca19cce257cc7309" + integrity sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg== dependencies: "@babel/types" "^7.3.0" @@ -1666,9 +1615,9 @@ "@types/istanbul-lib-report" "*" "@types/jest@^29.0.0": - version "29.0.3" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.0.3.tgz#b61a5ed100850686b8d3c5e28e3a1926b2001b59" - integrity sha512-F6ukyCTwbfsEX5F2YmVYmM5TcTHy1q9P5rWlRbrk56KyMh3v9xRGUO3aa8+SkvMi0SHXtASJv1283enXimC0Og== + version "29.1.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.1.1.tgz#cf21a0835a1ba9a30ea1966019f1261c6a114c92" + integrity sha512-U9Ey07dGWl6fUFaIaUQUKWG5NoKi/zizeVQCGV8s4nSU0jPgqphVZvS64+8BtWYvrc3ZGw6wo943NSYPxkrp/g== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -1702,19 +1651,19 @@ integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== "@types/node@*": - version "18.7.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154" - integrity sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg== + version "18.8.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.0.tgz#b8ee8d83a99470c0661bd899417fcd77060682fe" + integrity sha512-u+h43R6U8xXDt2vzUaVP3VwjjLyOJk6uEciZS8OSyziUQGOwmk+l+4drxcsDboHXwyTaqS1INebghmWMRxq3LA== "@types/node@16": - version "16.11.59" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.59.tgz#823f238b9063ccc3b3b7f13186f143a57926c4f6" - integrity sha512-6u+36Dj3aDzhfBVUf/mfmc92OEdzQ2kx2jcXGdigfl70E/neV21ZHE6UCz4MDzTRcVqGAM27fk+DLXvyDsn3Jw== + version "16.11.63" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.63.tgz#af57f6d2c3fb17a571230d527003dd734a9109b2" + integrity sha512-3OxnrEQLBz8EIIaHpg3CibmTAEGkDBcHY4fL5cnBwg2vd2yvHrUDGWxK+MlYPeXWWIoJJW79dGtU+oeBr6166Q== "@types/prettier@^2.1.5": - version "2.7.0" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.0.tgz#ea03e9f0376a4446f44797ca19d9c46c36e352dc" - integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A== + version "2.7.1" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e" + integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow== "@types/request@^2.48.5": version "2.48.8" @@ -1747,20 +1696,20 @@ integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^17.0.8": - version "17.0.12" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.12.tgz#0745ff3e4872b4ace98616d4b7e37ccbd75f9526" - integrity sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ== + version "17.0.13" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76" + integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg== dependencies: "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.6.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.0.tgz#ac919a199548861012e8c1fb2ec4899ac2bc22ae" - integrity sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ== + version "5.38.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz#9f05d42fa8fb9f62304cc2f5c2805e03c01c2620" + integrity sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ== dependencies: - "@typescript-eslint/scope-manager" "5.38.0" - "@typescript-eslint/type-utils" "5.38.0" - "@typescript-eslint/utils" "5.38.0" + "@typescript-eslint/scope-manager" "5.38.1" + "@typescript-eslint/type-utils" "5.38.1" + "@typescript-eslint/utils" "5.38.1" debug "^4.3.4" ignore "^5.2.0" regexpp "^3.2.0" @@ -1768,69 +1717,69 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.6.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.38.0.tgz#5a59a1ff41a7b43aacd1bb2db54f6bf1c02b2ff8" - integrity sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA== + version "5.38.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.38.1.tgz#c577f429f2c32071b92dff4af4f5fbbbd2414bd0" + integrity sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw== dependencies: - "@typescript-eslint/scope-manager" "5.38.0" - "@typescript-eslint/types" "5.38.0" - "@typescript-eslint/typescript-estree" "5.38.0" + "@typescript-eslint/scope-manager" "5.38.1" + "@typescript-eslint/types" "5.38.1" + "@typescript-eslint/typescript-estree" "5.38.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.38.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.38.0.tgz#8f0927024b6b24e28671352c93b393a810ab4553" - integrity sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA== +"@typescript-eslint/scope-manager@5.38.1": + version "5.38.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.38.1.tgz#f87b289ef8819b47189351814ad183e8801d5764" + integrity sha512-BfRDq5RidVU3RbqApKmS7RFMtkyWMM50qWnDAkKgQiezRtLKsoyRKIvz1Ok5ilRWeD9IuHvaidaLxvGx/2eqTQ== dependencies: - "@typescript-eslint/types" "5.38.0" - "@typescript-eslint/visitor-keys" "5.38.0" + "@typescript-eslint/types" "5.38.1" + "@typescript-eslint/visitor-keys" "5.38.1" -"@typescript-eslint/type-utils@5.38.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.38.0.tgz#c8b7f681da825fcfc66ff2b63d70693880496876" - integrity sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA== +"@typescript-eslint/type-utils@5.38.1": + version "5.38.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.38.1.tgz#7f038fcfcc4ade4ea76c7c69b2aa25e6b261f4c1" + integrity sha512-UU3j43TM66gYtzo15ivK2ZFoDFKKP0k03MItzLdq0zV92CeGCXRfXlfQX5ILdd4/DSpHkSjIgLLLh1NtkOJOAw== dependencies: - "@typescript-eslint/typescript-estree" "5.38.0" - "@typescript-eslint/utils" "5.38.0" + "@typescript-eslint/typescript-estree" "5.38.1" + "@typescript-eslint/utils" "5.38.1" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.38.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.38.0.tgz#8cd15825e4874354e31800dcac321d07548b8a5f" - integrity sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA== +"@typescript-eslint/types@5.38.1": + version "5.38.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.38.1.tgz#74f9d6dcb8dc7c58c51e9fbc6653ded39e2e225c" + integrity sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg== -"@typescript-eslint/typescript-estree@5.38.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.0.tgz#89f86b2279815c6fb7f57d68cf9b813f0dc25d98" - integrity sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg== +"@typescript-eslint/typescript-estree@5.38.1": + version "5.38.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.1.tgz#657d858d5d6087f96b638ee383ee1cff52605a1e" + integrity sha512-99b5e/Enoe8fKMLdSuwrfH/C0EIbpUWmeEKHmQlGZb8msY33qn1KlkFww0z26o5Omx7EVjzVDCWEfrfCDHfE7g== dependencies: - "@typescript-eslint/types" "5.38.0" - "@typescript-eslint/visitor-keys" "5.38.0" + "@typescript-eslint/types" "5.38.1" + "@typescript-eslint/visitor-keys" "5.38.1" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.38.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.38.0.tgz#5b31f4896471818153790700eb02ac869a1543f4" - integrity sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA== +"@typescript-eslint/utils@5.38.1": + version "5.38.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.38.1.tgz#e3ac37d7b33d1362bb5adf4acdbe00372fb813ef" + integrity sha512-oIuUiVxPBsndrN81oP8tXnFa/+EcZ03qLqPDfSZ5xIJVm7A9V0rlkQwwBOAGtrdN70ZKDlKv+l1BeT4eSFxwXA== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.38.0" - "@typescript-eslint/types" "5.38.0" - "@typescript-eslint/typescript-estree" "5.38.0" + "@typescript-eslint/scope-manager" "5.38.1" + "@typescript-eslint/types" "5.38.1" + "@typescript-eslint/typescript-estree" "5.38.1" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.38.0": - version "5.38.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.0.tgz#60591ca3bf78aa12b25002c0993d067c00887e34" - integrity sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w== +"@typescript-eslint/visitor-keys@5.38.1": + version "5.38.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.1.tgz#508071bfc6b96d194c0afe6a65ad47029059edbc" + integrity sha512-bSHr1rRxXt54+j2n4k54p4fj8AHJ49VDWtjpImOpzQj4qjAiOpPni+V1Tyajh19Api1i844F757cur8wH3YvOA== dependencies: - "@typescript-eslint/types" "5.38.0" + "@typescript-eslint/types" "5.38.1" eslint-visitor-keys "^3.3.0" JSONStream@^1.0.3: @@ -1842,9 +1791,9 @@ JSONStream@^1.0.3: through ">=2.2.7 <3" ace-builds@^1.4.13: - version "1.9.6" - resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.9.6.tgz#2d3721f90f0664b79be9288f6319dd57576ff1e7" - integrity sha512-M/Li4hPruMSbkkg35LgdbsIBq0WuwrV4ztP2pKaww47rC/MvDc1bOrYxwJrfgxdlzyLKrja5bn+9KwwuzqB2xQ== + version "1.11.2" + resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.11.2.tgz#93fd7b7770909f3a48a23d71bd212d1b77baaaf5" + integrity sha512-1VNeUF56b6gkaeeWJXMBBuz5n0ceDchjUwwVmTKpNM/N3YRrUEpykGEEsg7Y1PKP7IRyqtXfAu6VJDg7OZaLfA== acorn-globals@^3.0.0: version "3.1.0" @@ -2079,12 +2028,12 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -babel-jest@^29.0.0, babel-jest@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.0.3.tgz#64e156a47a77588db6a669a88dedff27ed6e260f" - integrity sha512-ApPyHSOhS/sVzwUOQIWJmdvDhBsMG01HX9z7ogtkp1TToHGGUWFlnXJUIzCgKPSfiYLn3ibipCYzsKSURHEwLg== +babel-jest@^29.0.0, babel-jest@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.1.2.tgz#540d3241925c55240fb0c742e3ffc5f33a501978" + integrity sha512-IuG+F3HTHryJb7gacC7SQ59A9kO56BctUsT67uJHp1mMCHUOMXpDwOHWGifWqdWVknN2WNkCVQELPjXx0aLJ9Q== dependencies: - "@jest/transform" "^29.0.3" + "@jest/transform" "^29.1.2" "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.1.1" babel-preset-jest "^29.0.2" @@ -2534,9 +2483,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001400: - version "1.0.30001409" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001409.tgz#6135da9dcab34cd9761d9cdb12a68e6740c5e96e" - integrity sha512-V0mnJ5dwarmhYv8/MzhJ//aW68UpvnQBXv8lJ2QUsvn2pHcmAuNtu8hQEDz37XnA1iE+lRR9CIfGWWpgJ5QedQ== + version "1.0.30001414" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001414.tgz#5f1715e506e71860b4b07c50060ea6462217611e" + integrity sha512-t55jfSaWjCdocnFdKQoO+d2ct9C59UZg4dY3OnUlSZ447r8pUtIKdp0hpAzrGFultmTC+Us+KpKi4GZl/LXlFg== caseless@~0.12.0: version "0.12.0" @@ -2656,6 +2605,15 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -2784,9 +2742,9 @@ convert-source-map@~1.1.0: integrity sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg== core-js-compat@^3.25.1: - version "3.25.2" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.25.2.tgz#7875573586809909c69e03ef310810c1969ee138" - integrity sha512-TxfyECD4smdn3/CjWxczVtJqVLEEC2up7/82t7vC0AzNogr+4nQ8vyF7abxAuTXWvjTClSbvGhU0RgqA4ToQaQ== + version "3.25.4" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.25.4.tgz#730a255d4a47a937513abf1672bf278dc24dcebf" + integrity sha512-gCEcIEEqCR6230WroNunK/653CWKhqyCKJ9b+uESqOt/WFJA8B4lTnnQFdpYY5vmBcwJAA90Bo5vXs+CVsf6iA== dependencies: browserslist "^4.21.4" @@ -3071,9 +3029,9 @@ ecc-jsbn@~0.1.1: safer-buffer "^2.1.0" electron-to-chromium@^1.4.251: - version "1.4.256" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.256.tgz#c735032f412505e8e0482f147a8ff10cfca45bf4" - integrity sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw== + version "1.4.270" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.270.tgz#2c6ea409b45cdb5c3e0cb2c08cf6c0ba7e0f2c26" + integrity sha512-KNhIzgLiJmDDC444dj9vEOpZEgsV96ult9Iff98Vanumn+ShJHd5se8aX6KeVxdc0YQeqdrezBZv89rleDbvSg== elliptic@^6.5.3: version "6.5.4" @@ -3119,30 +3077,31 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.0: - version "1.20.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" - integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== + version "1.20.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.3.tgz#90b143ff7aedc8b3d189bcfac7f1e3e3f81e9da1" + integrity sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" function-bind "^1.1.1" function.prototype.name "^1.1.5" - get-intrinsic "^1.1.1" + get-intrinsic "^1.1.3" get-symbol-description "^1.0.0" has "^1.0.3" has-property-descriptors "^1.0.0" has-symbols "^1.0.3" internal-slot "^1.0.3" - is-callable "^1.2.4" + is-callable "^1.2.6" is-negative-zero "^2.0.2" is-regex "^1.1.4" is-shared-array-buffer "^1.0.2" is-string "^1.0.7" is-weakref "^1.0.2" - object-inspect "^1.12.0" + object-inspect "^1.12.2" object-keys "^1.1.1" - object.assign "^4.1.2" + object.assign "^4.1.4" regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" string.prototype.trimend "^1.0.5" string.prototype.trimstart "^1.0.5" unbox-primitive "^1.0.2" @@ -3473,16 +3432,16 @@ expect@^28.1.0: jest-message-util "^28.1.3" jest-util "^28.1.3" -expect@^29.0.0, expect@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.0.3.tgz#6be65ddb945202f143c4e07c083f4f39f3bd326f" - integrity sha512-t8l5DTws3212VbmPL+tBFXhjRHLmctHB0oQbL8eUc6S7NzZtYUhycrFO9mkxA0ZUC6FAWdNi7JchJSkODtcu1Q== +expect@^29.0.0, expect@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.1.2.tgz#82f8f28d7d408c7c68da3a386a490ee683e1eced" + integrity sha512-AuAGn1uxva5YBbBlXb+2JPxJRuemZsmlGcapPXWNSBNsQtAULfjioREGBWuI0EOvYUKjDnrCy8PW5Zlr1md5mw== dependencies: - "@jest/expect-utils" "^29.0.3" + "@jest/expect-utils" "^29.1.2" jest-get-type "^29.0.0" - jest-matcher-utils "^29.0.3" - jest-message-util "^29.0.3" - jest-util "^29.0.3" + jest-matcher-utils "^29.1.2" + jest-message-util "^29.1.2" + jest-util "^29.1.2" ext@^1.1.2: version "1.7.0" @@ -3552,9 +3511,9 @@ fastq@^1.6.0: reusify "^1.0.4" fb-watchman@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" - integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== dependencies: bser "2.1.1" @@ -3705,7 +3664,7 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, 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== @@ -3714,15 +3673,6 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.3" -get-intrinsic@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" - integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" - get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -4074,10 +4024,10 @@ is-buffer@^1.1.0, is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" - integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.6: + version "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.10.0, is-core-module@^2.8.1, is-core-module@^2.9.0: version "2.10.0" @@ -4308,74 +4258,74 @@ jest-changed-files@^29.0.0: execa "^5.0.0" p-limit "^3.1.0" -jest-circus@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.0.3.tgz#90faebc90295291cfc636b27dbd82e3bfb9e7a48" - integrity sha512-QeGzagC6Hw5pP+df1+aoF8+FBSgkPmraC1UdkeunWh0jmrp7wC0Hr6umdUAOELBQmxtKAOMNC3KAdjmCds92Zg== +jest-circus@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.1.2.tgz#4551068e432f169a53167fe1aef420cf51c8a735" + integrity sha512-ajQOdxY6mT9GtnfJRZBRYS7toNIJayiiyjDyoZcnvPRUPwJ58JX0ci0PKAKUo2C1RyzlHw0jabjLGKksO42JGA== dependencies: - "@jest/environment" "^29.0.3" - "@jest/expect" "^29.0.3" - "@jest/test-result" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/environment" "^29.1.2" + "@jest/expect" "^29.1.2" + "@jest/test-result" "^29.1.2" + "@jest/types" "^29.1.2" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" is-generator-fn "^2.0.0" - jest-each "^29.0.3" - jest-matcher-utils "^29.0.3" - jest-message-util "^29.0.3" - jest-runtime "^29.0.3" - jest-snapshot "^29.0.3" - jest-util "^29.0.3" + jest-each "^29.1.2" + jest-matcher-utils "^29.1.2" + jest-message-util "^29.1.2" + jest-runtime "^29.1.2" + jest-snapshot "^29.1.2" + jest-util "^29.1.2" p-limit "^3.1.0" - pretty-format "^29.0.3" + pretty-format "^29.1.2" slash "^3.0.0" stack-utils "^2.0.3" -jest-cli@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.0.3.tgz#fd8f0ef363a7a3d9c53ef62e0651f18eeffa77b9" - integrity sha512-aUy9Gd/Kut1z80eBzG10jAn6BgS3BoBbXyv+uXEqBJ8wnnuZ5RpNfARoskSrTIy1GY4a8f32YGuCMwibtkl9CQ== +jest-cli@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.1.2.tgz#423b9c5d3ea20a50b1354b8bf3f2a20e72110e89" + integrity sha512-vsvBfQ7oS2o4MJdAH+4u9z76Vw5Q8WBQF5MchDbkylNknZdrPTX1Ix7YRJyTlOWqRaS7ue/cEAn+E4V1MWyMzw== dependencies: - "@jest/core" "^29.0.3" - "@jest/test-result" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/core" "^29.1.2" + "@jest/test-result" "^29.1.2" + "@jest/types" "^29.1.2" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^29.0.3" - jest-util "^29.0.3" - jest-validate "^29.0.3" + jest-config "^29.1.2" + jest-util "^29.1.2" + jest-validate "^29.1.2" prompts "^2.0.1" yargs "^17.3.1" -jest-config@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.0.3.tgz#c2e52a8f5adbd18de79f99532d8332a19e232f13" - integrity sha512-U5qkc82HHVYe3fNu2CRXLN4g761Na26rWKf7CjM8LlZB3In1jadEkZdMwsE37rd9RSPV0NfYaCjHdk/gu3v+Ew== +jest-config@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.1.2.tgz#7d004345ca4c09f5d8f802355f54494e90842f4d" + integrity sha512-EC3Zi86HJUOz+2YWQcJYQXlf0zuBhJoeyxLM6vb6qJsVmpP7KcCP1JnyF0iaqTaXdBP8Rlwsvs7hnKWQWWLwwA== dependencies: "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.0.3" - "@jest/types" "^29.0.3" - babel-jest "^29.0.3" + "@jest/test-sequencer" "^29.1.2" + "@jest/types" "^29.1.2" + babel-jest "^29.1.2" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.3" graceful-fs "^4.2.9" - jest-circus "^29.0.3" - jest-environment-node "^29.0.3" + jest-circus "^29.1.2" + jest-environment-node "^29.1.2" jest-get-type "^29.0.0" jest-regex-util "^29.0.0" - jest-resolve "^29.0.3" - jest-runner "^29.0.3" - jest-util "^29.0.3" - jest-validate "^29.0.3" + jest-resolve "^29.1.2" + jest-runner "^29.1.2" + jest-util "^29.1.2" + jest-validate "^29.1.2" micromatch "^4.0.4" parse-json "^5.2.0" - pretty-format "^29.0.3" + pretty-format "^29.1.2" slash "^3.0.0" strip-json-comments "^3.1.1" @@ -4389,15 +4339,15 @@ jest-diff@^28.1.3: jest-get-type "^28.0.2" pretty-format "^28.1.3" -jest-diff@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.0.3.tgz#41cc02409ad1458ae1bf7684129a3da2856341ac" - integrity sha512-+X/AIF5G/vX9fWK+Db9bi9BQas7M9oBME7egU7psbn4jlszLFCu0dW63UgeE6cs/GANq4fLaT+8sGHQQ0eCUfg== +jest-diff@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.1.2.tgz#bb7aaf5353227d6f4f96c5e7e8713ce576a607dc" + integrity sha512-4GQts0aUopVvecIT4IwD/7xsBaMhKTYoM4/njE/aVw9wpw+pIUVp8Vab/KnSzSilr84GnLBkaP3JLDnQYCKqVQ== dependencies: chalk "^4.0.0" diff-sequences "^29.0.0" jest-get-type "^29.0.0" - pretty-format "^29.0.3" + pretty-format "^29.1.2" jest-docblock@^29.0.0: version "29.0.0" @@ -4406,28 +4356,28 @@ jest-docblock@^29.0.0: dependencies: detect-newline "^3.0.0" -jest-each@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.0.3.tgz#7ef3157580b15a609d7ef663dd4fc9b07f4e1299" - integrity sha512-wILhZfESURHHBNvPMJ0lZlYZrvOQJxAo3wNHi+ycr90V7M+uGR9Gh4+4a/BmaZF0XTyZsk4OiYEf3GJN7Ltqzg== +jest-each@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.1.2.tgz#d4c8532c07a846e79f194f7007ce7cb1987d1cd0" + integrity sha512-AmTQp9b2etNeEwMyr4jc0Ql/LIX/dhbgP21gHAizya2X6rUspHn2gysMXaj6iwWuOJ2sYRgP8c1P4cXswgvS1A== dependencies: - "@jest/types" "^29.0.3" + "@jest/types" "^29.1.2" chalk "^4.0.0" jest-get-type "^29.0.0" - jest-util "^29.0.3" - pretty-format "^29.0.3" + jest-util "^29.1.2" + pretty-format "^29.1.2" -jest-environment-node@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.0.3.tgz#293804b1e0fa5f0e354dacbe510655caa478a3b2" - integrity sha512-cdZqRCnmIlTXC+9vtvmfiY/40Cj6s2T0czXuq1whvQdmpzAnj4sbqVYuZ4zFHk766xTTJ+Ij3uUqkk8KCfXoyg== +jest-environment-node@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.1.2.tgz#005e05cc6ea4b9b5ba55906ab1ce53c82f6907a7" + integrity sha512-C59yVbdpY8682u6k/lh8SUMDJPbOyCHOTgLVVi1USWFxtNV+J8fyIwzkg+RJIVI30EKhKiAGNxYaFr3z6eyNhQ== dependencies: - "@jest/environment" "^29.0.3" - "@jest/fake-timers" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/environment" "^29.1.2" + "@jest/fake-timers" "^29.1.2" + "@jest/types" "^29.1.2" "@types/node" "*" - jest-mock "^29.0.3" - jest-util "^29.0.3" + jest-mock "^29.1.2" + jest-util "^29.1.2" jest-get-type@^28.0.2: version "28.0.2" @@ -4439,32 +4389,32 @@ jest-get-type@^29.0.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.0.0.tgz#843f6c50a1b778f7325df1129a0fd7aa713aef80" integrity sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw== -jest-haste-map@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.0.3.tgz#d7f3f7180f558d760eacc5184aac5a67f20ef939" - integrity sha512-uMqR99+GuBHo0RjRhOE4iA6LmsxEwRdgiIAQgMU/wdT2XebsLDz5obIwLZm/Psj+GwSEQhw9AfAVKGYbh2G55A== +jest-haste-map@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.1.2.tgz#93f3634aa921b6b654e7c94137b24e02e7ca6ac9" + integrity sha512-xSjbY8/BF11Jh3hGSPfYTa/qBFrm3TPM7WU8pU93m2gqzORVLkHFWvuZmFsTEBPRKndfewXhMOuzJNHyJIZGsw== dependencies: - "@jest/types" "^29.0.3" + "@jest/types" "^29.1.2" "@types/graceful-fs" "^4.1.3" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.9" jest-regex-util "^29.0.0" - jest-util "^29.0.3" - jest-worker "^29.0.3" + jest-util "^29.1.2" + jest-worker "^29.1.2" micromatch "^4.0.4" walker "^1.0.8" optionalDependencies: fsevents "^2.3.2" -jest-leak-detector@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.0.3.tgz#e85cf3391106a7a250850b6766b508bfe9c7bc6f" - integrity sha512-YfW/G63dAuiuQ3QmQlh8hnqLDe25WFY3eQhuc/Ev1AGmkw5zREblTh7TCSKLoheyggu6G9gxO2hY8p9o6xbaRQ== +jest-leak-detector@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.1.2.tgz#4c846db14c58219430ccbc4f01a1ec52ebee4fc2" + integrity sha512-TG5gAZJpgmZtjb6oWxBLf2N6CfQ73iwCe6cofu/Uqv9iiAm6g502CAnGtxQaTfpHECBdVEMRBhomSXeLnoKjiQ== dependencies: jest-get-type "^29.0.0" - pretty-format "^29.0.3" + pretty-format "^29.1.2" jest-localstorage-mock@^2.4.6: version "2.4.22" @@ -4481,15 +4431,15 @@ jest-matcher-utils@^28.1.3: jest-get-type "^28.0.2" pretty-format "^28.1.3" -jest-matcher-utils@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.0.3.tgz#b8305fd3f9e27cdbc210b21fc7dbba92d4e54560" - integrity sha512-RsR1+cZ6p1hDV4GSCQTg+9qjeotQCgkaleIKLK7dm+U4V/H2bWedU3RAtLm8+mANzZ7eDV33dMar4pejd7047w== +jest-matcher-utils@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.1.2.tgz#e68c4bcc0266e70aa1a5c13fb7b8cd4695e318a1" + integrity sha512-MV5XrD3qYSW2zZSHRRceFzqJ39B2z11Qv0KPyZYxnzDHFeYZGJlgGi0SW+IXSJfOewgJp/Km/7lpcFT+cgZypw== dependencies: chalk "^4.0.0" - jest-diff "^29.0.3" + jest-diff "^29.1.2" jest-get-type "^29.0.0" - pretty-format "^29.0.3" + pretty-format "^29.1.2" jest-message-util@^28.1.3: version "28.1.3" @@ -4506,28 +4456,29 @@ jest-message-util@^28.1.3: slash "^3.0.0" stack-utils "^2.0.3" -jest-message-util@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.0.3.tgz#f0254e1ffad21890c78355726202cc91d0a40ea8" - integrity sha512-7T8JiUTtDfppojosORAflABfLsLKMLkBHSWkjNQrjIltGoDzNGn7wEPOSfjqYAGTYME65esQzMJxGDjuLBKdOg== +jest-message-util@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.1.2.tgz#c21a33c25f9dc1ebfcd0f921d89438847a09a501" + integrity sha512-9oJ2Os+Qh6IlxLpmvshVbGUiSkZVc2FK+uGOm6tghafnB2RyjKAxMZhtxThRMxfX1J1SOMhTn9oK3/MutRWQJQ== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.0.3" + "@jest/types" "^29.1.2" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.9" micromatch "^4.0.4" - pretty-format "^29.0.3" + pretty-format "^29.1.2" slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.0.3.tgz#4f0093f6a9cb2ffdb9c44a07a3912f0c098c8de9" - integrity sha512-ort9pYowltbcrCVR43wdlqfAiFJXBx8l4uJDsD8U72LgBcetvEp+Qxj1W9ZYgMRoeAo+ov5cnAGF2B6+Oth+ww== +jest-mock@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.1.2.tgz#de47807edbb9d4abf8423f1d8d308d670105678c" + integrity sha512-PFDAdjjWbjPUtQPkQufvniXIS3N9Tv7tbibePEjIIprzjgo0qQlyUiVMrT4vL8FaSJo1QXifQUOuPH3HQC/aMA== dependencies: - "@jest/types" "^29.0.3" + "@jest/types" "^29.1.2" "@types/node" "*" + jest-util "^29.1.2" jest-pnp-resolver@^1.2.2: version "1.2.2" @@ -4539,88 +4490,88 @@ jest-regex-util@^29.0.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.0.0.tgz#b442987f688289df8eb6c16fa8df488b4cd007de" integrity sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug== -jest-resolve-dependencies@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.0.3.tgz#f23a54295efc6374b86b198cf8efed5606d6b762" - integrity sha512-KzuBnXqNvbuCdoJpv8EanbIGObk7vUBNt/PwQPPx2aMhlv/jaXpUJsqWYRpP/0a50faMBY7WFFP8S3/CCzwfDw== +jest-resolve-dependencies@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.1.2.tgz#a6919e58a0c7465582cb8ec2d745b4e64ae8647f" + integrity sha512-44yYi+yHqNmH3OoWZvPgmeeiwKxhKV/0CfrzaKLSkZG9gT973PX8i+m8j6pDrTYhhHoiKfF3YUFg/6AeuHw4HQ== dependencies: jest-regex-util "^29.0.0" - jest-snapshot "^29.0.3" + jest-snapshot "^29.1.2" -jest-resolve@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.0.3.tgz#329a3431e3b9eb6629a2cd483e9bed95b26827b9" - integrity sha512-toVkia85Y/BPAjJasTC9zIPY6MmVXQPtrCk8SmiheC4MwVFE/CMFlOtMN6jrwPMC6TtNh8+sTMllasFeu1wMPg== +jest-resolve@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.1.2.tgz#9dd8c2fc83e59ee7d676b14bd45a5f89e877741d" + integrity sha512-7fcOr+k7UYSVRJYhSmJHIid3AnDBcLQX3VmT9OSbPWsWz1MfT7bcoerMhADKGvKCoMpOHUQaDHtQoNp/P9JMGg== dependencies: chalk "^4.0.0" graceful-fs "^4.2.9" - jest-haste-map "^29.0.3" + jest-haste-map "^29.1.2" jest-pnp-resolver "^1.2.2" - jest-util "^29.0.3" - jest-validate "^29.0.3" + jest-util "^29.1.2" + jest-validate "^29.1.2" resolve "^1.20.0" resolve.exports "^1.1.0" slash "^3.0.0" -jest-runner@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.0.3.tgz#2e47fe1e8777aea9b8970f37e8f83630b508fb87" - integrity sha512-Usu6VlTOZlCZoNuh3b2Tv/yzDpKqtiNAetG9t3kJuHfUyVMNW7ipCCJOUojzKkjPoaN7Bl1f7Buu6PE0sGpQxw== +jest-runner@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.1.2.tgz#f18b2b86101341e047de8c2f51a5fdc4e97d053a" + integrity sha512-yy3LEWw8KuBCmg7sCGDIqKwJlULBuNIQa2eFSVgVASWdXbMYZ9H/X0tnXt70XFoGf92W2sOQDOIFAA6f2BG04Q== dependencies: - "@jest/console" "^29.0.3" - "@jest/environment" "^29.0.3" - "@jest/test-result" "^29.0.3" - "@jest/transform" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/console" "^29.1.2" + "@jest/environment" "^29.1.2" + "@jest/test-result" "^29.1.2" + "@jest/transform" "^29.1.2" + "@jest/types" "^29.1.2" "@types/node" "*" chalk "^4.0.0" emittery "^0.10.2" graceful-fs "^4.2.9" jest-docblock "^29.0.0" - jest-environment-node "^29.0.3" - jest-haste-map "^29.0.3" - jest-leak-detector "^29.0.3" - jest-message-util "^29.0.3" - jest-resolve "^29.0.3" - jest-runtime "^29.0.3" - jest-util "^29.0.3" - jest-watcher "^29.0.3" - jest-worker "^29.0.3" + jest-environment-node "^29.1.2" + jest-haste-map "^29.1.2" + jest-leak-detector "^29.1.2" + jest-message-util "^29.1.2" + jest-resolve "^29.1.2" + jest-runtime "^29.1.2" + jest-util "^29.1.2" + jest-watcher "^29.1.2" + jest-worker "^29.1.2" p-limit "^3.1.0" source-map-support "0.5.13" -jest-runtime@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.0.3.tgz#5a823ec5902257519556a4e5a71a868e8fd788aa" - integrity sha512-12gZXRQ7ozEeEHKTY45a+YLqzNDR/x4c//X6AqwKwKJPpWM8FY4vwn4VQJOcLRS3Nd1fWwgP7LU4SoynhuUMHQ== +jest-runtime@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.1.2.tgz#dbcd57103d61115479108d5864bdcd661d9c6783" + integrity sha512-jr8VJLIf+cYc+8hbrpt412n5jX3tiXmpPSYTGnwcvNemY+EOuLNiYnHJ3Kp25rkaAcTWOEI4ZdOIQcwYcXIAZw== dependencies: - "@jest/environment" "^29.0.3" - "@jest/fake-timers" "^29.0.3" - "@jest/globals" "^29.0.3" + "@jest/environment" "^29.1.2" + "@jest/fake-timers" "^29.1.2" + "@jest/globals" "^29.1.2" "@jest/source-map" "^29.0.0" - "@jest/test-result" "^29.0.3" - "@jest/transform" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/test-result" "^29.1.2" + "@jest/transform" "^29.1.2" + "@jest/types" "^29.1.2" "@types/node" "*" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" glob "^7.1.3" graceful-fs "^4.2.9" - jest-haste-map "^29.0.3" - jest-message-util "^29.0.3" - jest-mock "^29.0.3" + jest-haste-map "^29.1.2" + jest-message-util "^29.1.2" + jest-mock "^29.1.2" jest-regex-util "^29.0.0" - jest-resolve "^29.0.3" - jest-snapshot "^29.0.3" - jest-util "^29.0.3" + jest-resolve "^29.1.2" + jest-snapshot "^29.1.2" + jest-util "^29.1.2" slash "^3.0.0" strip-bom "^4.0.0" -jest-snapshot@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.0.3.tgz#0a024706986a915a6eefae74d7343069d2fc8eef" - integrity sha512-52q6JChm04U3deq+mkQ7R/7uy7YyfVIrebMi6ZkBoDJ85yEjm/sJwdr1P0LOIEHmpyLlXrxy3QP0Zf5J2kj0ew== +jest-snapshot@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.1.2.tgz#7dd277e88c45f2d2ff5888de1612e63c7ceb575b" + integrity sha512-rYFomGpVMdBlfwTYxkUp3sjD6usptvZcONFYNqVlaz4EpHPnDvlWjvmOQ9OCSNKqYZqLM2aS3wq01tWujLg7gg== dependencies: "@babel/core" "^7.11.6" "@babel/generator" "^7.7.2" @@ -4628,23 +4579,23 @@ jest-snapshot@^29.0.3: "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/traverse" "^7.7.2" "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.0.3" - "@jest/transform" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/expect-utils" "^29.1.2" + "@jest/transform" "^29.1.2" + "@jest/types" "^29.1.2" "@types/babel__traverse" "^7.0.6" "@types/prettier" "^2.1.5" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^29.0.3" + expect "^29.1.2" graceful-fs "^4.2.9" - jest-diff "^29.0.3" + jest-diff "^29.1.2" jest-get-type "^29.0.0" - jest-haste-map "^29.0.3" - jest-matcher-utils "^29.0.3" - jest-message-util "^29.0.3" - jest-util "^29.0.3" + jest-haste-map "^29.1.2" + jest-matcher-utils "^29.1.2" + jest-message-util "^29.1.2" + jest-util "^29.1.2" natural-compare "^1.4.0" - pretty-format "^29.0.3" + pretty-format "^29.1.2" semver "^7.3.5" jest-sonar-reporter@^2.0.0: @@ -4666,67 +4617,68 @@ jest-util@^28.1.3: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-util@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.0.3.tgz#06d1d77f9a1bea380f121897d78695902959fbc0" - integrity sha512-Q0xaG3YRG8QiTC4R6fHjHQPaPpz9pJBEi0AeOE4mQh/FuWOijFjGXMMOfQEaU9i3z76cNR7FobZZUQnL6IyfdQ== +jest-util@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.1.2.tgz#ac5798e93cb6a6703084e194cfa0898d66126df1" + integrity sha512-vPCk9F353i0Ymx3WQq3+a4lZ07NXu9Ca8wya6o4Fe4/aO1e1awMMprZ3woPFpKwghEOW+UXgd15vVotuNN9ONQ== dependencies: - "@jest/types" "^29.0.3" + "@jest/types" "^29.1.2" "@types/node" "*" chalk "^4.0.0" ci-info "^3.2.0" graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.0.3.tgz#f9521581d7344685428afa0a4d110e9c519aeeb6" - integrity sha512-OebiqqT6lK8cbMPtrSoS3aZP4juID762lZvpf1u+smZnwTEBCBInan0GAIIhv36MxGaJvmq5uJm7dl5gVt+Zrw== +jest-validate@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.1.2.tgz#83a728b8f6354da2e52346878c8bc7383516ca51" + integrity sha512-k71pOslNlV8fVyI+mEySy2pq9KdXdgZtm7NHrBX8LghJayc3wWZH0Yr0mtYNGaCU4F1OLPXRkwZR0dBm/ClshA== dependencies: - "@jest/types" "^29.0.3" + "@jest/types" "^29.1.2" camelcase "^6.2.0" chalk "^4.0.0" jest-get-type "^29.0.0" leven "^3.1.0" - pretty-format "^29.0.3" + pretty-format "^29.1.2" -jest-watcher@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.0.3.tgz#8e220d1cc4f8029875e82015d084cab20f33d57f" - integrity sha512-tQX9lU91A+9tyUQKUMp0Ns8xAcdhC9fo73eqA3LFxP2bSgiF49TNcc+vf3qgGYYK9qRjFpXW9+4RgF/mbxyOOw== +jest-watcher@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.1.2.tgz#de21439b7d889e2fcf62cc2a4779ef1a3f1f3c62" + integrity sha512-6JUIUKVdAvcxC6bM8/dMgqY2N4lbT+jZVsxh0hCJRbwkIEnbr/aPjMQ28fNDI5lB51Klh00MWZZeVf27KBUj5w== dependencies: - "@jest/test-result" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/test-result" "^29.1.2" + "@jest/types" "^29.1.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.10.2" - jest-util "^29.0.3" + jest-util "^29.1.2" string-length "^4.0.1" -jest-worker@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.0.3.tgz#c2ba0aa7e41eec9eb0be8e8a322ae6518df72647" - integrity sha512-Tl/YWUugQOjoTYwjKdfJWkSOfhufJHO5LhXTSZC3TRoQKO+fuXnZAdoXXBlpLXKGODBL3OvdUasfDD4PcMe6ng== +jest-worker@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.1.2.tgz#a68302af61bce82b42a9a57285ca7499d29b2afc" + integrity sha512-AdTZJxKjTSPHbXT/AIOjQVmoFx0LHFcVabWu0sxI7PAy7rFf8c0upyvgBKgguVXdM4vY74JdwkyD4hSmpTW8jA== dependencies: "@types/node" "*" + jest-util "^29.1.2" merge-stream "^2.0.0" supports-color "^8.0.0" jest@^29.0.0: - version "29.0.3" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.0.3.tgz#5227a0596d30791b2649eea347e4aa97f734944d" - integrity sha512-ElgUtJBLgXM1E8L6K1RW1T96R897YY/3lRYqq9uVcPWtP2AAl/nQ16IYDh/FzQOOQ12VEuLdcPU83mbhG2C3PQ== + version "29.1.2" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.1.2.tgz#f821a1695ffd6cd0efc3b59d2dfcc70a98582499" + integrity sha512-5wEIPpCezgORnqf+rCaYD1SK+mNN7NsstWzIsuvsnrhR/hSxXWd82oI7DkrbJ+XTD28/eG8SmxdGvukrGGK6Tw== dependencies: - "@jest/core" "^29.0.3" - "@jest/types" "^29.0.3" + "@jest/core" "^29.1.2" + "@jest/types" "^29.1.2" import-local "^3.0.2" - jest-cli "^29.0.3" + jest-cli "^29.1.2" js-sdsl@^4.1.4: - version "4.1.4" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.4.tgz#78793c90f80e8430b7d8dc94515b6c77d98a26a6" - integrity sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw== + version "4.1.5" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.5.tgz#1ff1645e6b4d1b028cd3f862db88c9d887f26e2a" + integrity sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q== js-stringify@^1.0.1: version "1.0.2" @@ -5032,9 +4984,9 @@ makeerror@1.0.12: tmpl "1.0.5" markdown-it-anchor@^8.4.1: - version "8.6.4" - resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.4.tgz#affb8aa0910a504c114e9fcad53ac3a5b907b0e6" - integrity sha512-Ul4YVYZNxMJYALpKtu+ZRdrryYt/GlQ5CK+4l1bp/gWXOG2QWElt6AqF3Mih/wfUKdZbNAZVXGR73/n6U/8img== + version "8.6.5" + resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.5.tgz#30c4bc5bbff327f15ce3c429010ec7ba75e7b5f8" + integrity sha512-PI1qEHHkTNWT+X6Ip9w+paonfIQ+QZP9sCeMYi47oqhH+EsW8CrJ8J7CzV19QVOj6il8ATGbK2nTECj22ZHGvQ== markdown-it@^12.3.2: version "12.3.2" @@ -5048,9 +5000,9 @@ markdown-it@^12.3.2: uc.micro "^1.0.5" marked@^4.0.10: - version "4.0.19" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.19.tgz#d36198d1ac1255525153c351c68c75bc1d7aee46" - integrity sha512-rgQF/OxOiLcvgUAj1Q1tAf4Bgxn5h5JZTp04Fx4XUkVhs7B+7YA9JEWJhJpoO8eJt8MkZMwqLCNeNqj1bCREZQ== + version "4.1.1" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.1.1.tgz#2f709a4462abf65a283f2453dc1c42ab177d302e" + integrity sha512-0cNMnTcUJPxbA6uWmCmjWz4NJRe/0Xfk2NhXCUHjew9qJzFN20krFnsUe7QynwqOwa5m1fZ4UDg0ycKFVC0ccw== matrix-events-sdk@^0.0.1-beta.7: version "0.0.1-beta.7" @@ -5277,7 +5229,7 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.12.0, object-inspect@^1.9.0: +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== @@ -5287,7 +5239,7 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.0, object.assign@^4.1.2: +object.assign@^4.1.0, object.assign@^4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== @@ -5546,10 +5498,10 @@ pretty-format@^28.1.3: ansi-styles "^5.0.0" react-is "^18.0.0" -pretty-format@^29.0.0, pretty-format@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.0.3.tgz#23d5f8cabc9cbf209a77d49409d093d61166a811" - integrity sha512-cHudsvQr1K5vNVLbvYF/nv3Qy/F/BcEKxGuIeMiVMRHxPOO1RxXooP8g/ZrwAp7Dx+KdMZoOc7NxLHhMrP2f9Q== +pretty-format@^29.0.0, pretty-format@^29.1.2: + version "29.1.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.1.2.tgz#b1f6b75be7d699be1a051f5da36e8ae9e76a8e6a" + integrity sha512-CGJ6VVGXVRP2o2Dorl4mAwwvDWT25luIsYhkyVQW32E4nL+TgW939J7LlKT/npq5Cpq6j3s+sy+13yk7xYpBmg== dependencies: "@jest/schemas" "^29.0.0" ansi-styles "^5.0.0" @@ -6065,6 +6017,15 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -6652,9 +6613,9 @@ typescript@^3.2.2: integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== typescript@^4.5.3, typescript@^4.5.4: - version "4.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88" - integrity sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig== + version "4.8.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" + integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== typeson-registry@^1.0.0-alpha.20: version "1.0.0-alpha.39" @@ -6717,9 +6678,9 @@ undeclared-identifiers@^1.1.2: xtend "^4.0.1" underscore@^1.13.2, underscore@~1.13.2: - version "1.13.4" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.4.tgz#7886b46bbdf07f768e0052f1828e1dcab40c0dee" - integrity sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ== + version "1.13.6" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" + integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== unhomoglyph@^1.0.6: version "1.0.6" @@ -7038,11 +6999,11 @@ yargs@^16.2.0: yargs-parser "^20.2.2" yargs@^17.0.1, yargs@^17.3.1: - version "17.5.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" - integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== + version "17.6.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.0.tgz#e134900fc1f218bc230192bdec06a0a5f973e46c" + integrity sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g== dependencies: - cliui "^7.0.2" + cliui "^8.0.1" escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" From 7d5360a00fc4af73b960ea9320c15ee31d1ff67c Mon Sep 17 00:00:00 2001 From: Faye Duxovni Date: Mon, 3 Oct 2022 19:51:30 -0400 Subject: [PATCH 13/48] Rename redecryption-related function arguments for clarity (#2709) --- src/crypto/algorithms/megolm.ts | 11 ++++++++--- src/models/event.ts | 8 ++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index 2052ee967..dbd9c6596 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -1795,12 +1795,17 @@ class MegolmDecryption extends DecryptionAlgorithm { * @private * @param {String} senderKey * @param {String} sessionId - * @param {Boolean} keyTrusted + * @param {Boolean} forceRedecryptIfUntrusted whether messages that were already + * successfully decrypted using untrusted keys should be re-decrypted * * @return {Boolean} whether all messages were successfully * decrypted with trusted keys */ - private async retryDecryption(senderKey: string, sessionId: string, keyTrusted?: boolean): Promise { + private async retryDecryption( + senderKey: string, + sessionId: string, + forceRedecryptIfUntrusted?: boolean, + ): Promise { const senderPendingEvents = this.pendingEvents.get(senderKey); if (!senderPendingEvents) { return true; @@ -1815,7 +1820,7 @@ class MegolmDecryption extends DecryptionAlgorithm { await Promise.all([...pending].map(async (ev) => { try { - await ev.attemptDecryption(this.crypto, { isRetry: true, keyTrusted }); + await ev.attemptDecryption(this.crypto, { isRetry: true, forceRedecryptIfUntrusted }); } catch (e) { // don't die if something goes wrong } diff --git a/src/models/event.ts b/src/models/event.ts index 071134978..406aded0b 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -151,7 +151,7 @@ interface IKeyRequestRecipient { export interface IDecryptOptions { emit?: boolean; isRetry?: boolean; - keyTrusted?: boolean; + forceRedecryptIfUntrusted?: boolean; } /** @@ -678,6 +678,8 @@ export class MatrixEvent extends TypedEventEmitter Date: Tue, 4 Oct 2022 08:50:42 +0200 Subject: [PATCH 14/48] Check for the spec version when determining private read receipt support (#2722) --- src/client.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index e0e051126..ab86cf3b1 100644 --- a/src/client.ts +++ b/src/client.ts @@ -7607,7 +7607,10 @@ export class MatrixClient extends TypedEventEmitter Date: Tue, 4 Oct 2022 09:59:20 +0100 Subject: [PATCH 15/48] Add custom error message for restricted import (#2727) --- .eslintrc.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 2baeaaac8..91993d504 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -43,7 +43,10 @@ module.exports = { "no-console": "error", // restrict EventEmitters to force callers to use TypedEventEmitter - "no-restricted-imports": ["error", "events"], + "no-restricted-imports": ["error", { + name: "events", + message: "Please use TypedEventEmitter instead" + }], "import/no-restricted-paths": ["error", { "zones": [{ From f84a33910c1cb1bcce728da2678edb3a94247859 Mon Sep 17 00:00:00 2001 From: mcalinghee Date: Tue, 4 Oct 2022 14:31:21 +0200 Subject: [PATCH 16/48] Unexpected ignored self key request when it's not shared history (#2724) * ignore forwarded key process also if the user is not the same --- src/crypto/algorithms/megolm.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index dbd9c6596..a831c5650 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -1444,9 +1444,10 @@ class MegolmDecryption extends DecryptionAlgorithm { memberEvent?.getPrevContent()?.membership === "invite"); const fromUs = event.getSender() === this.baseApis.getUserId(); - if (!weRequested) { - // If someone sends us an unsolicited key and it's not - // shared history, ignore it + if (!weRequested && !fromUs) { + // If someone sends us an unsolicited key and they're + // not one of our other devices and it's not shared + // history, ignore it if (!extraSessionData.sharedHistory) { logger.log("forwarded key not shared history - ignoring"); return; @@ -1455,7 +1456,7 @@ class MegolmDecryption extends DecryptionAlgorithm { // If someone sends us an unsolicited key for a room // we're already in, and they're not one of our other // devices or the one who invited us, ignore it - if (room && !fromInviter && !fromUs) { + if (room && !fromInviter) { logger.log("forwarded key not from inviter or from us - ignoring"); return; } From 6348704becbbd9948a0a440077a682a5fea7a5ed Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 4 Oct 2022 14:01:34 +0100 Subject: [PATCH 17/48] Prepare changelog for v20.1.0-rc.1 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9071a7ae9..35c5d440c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +Changes in [20.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0-rc.1) (2022-10-04) +============================================================================================================ + +## ✨ Features + * Add local notification settings capability ([\#2700](https://github.com/matrix-org/matrix-js-sdk/pull/2700)). + * Implementation of MSC3882 login token request ([\#2687](https://github.com/matrix-org/matrix-js-sdk/pull/2687)). Contributed by @hughns. + * Typings for MSC2965 OIDC provider discovery ([\#2424](https://github.com/matrix-org/matrix-js-sdk/pull/2424)). Contributed by @hughns. + * Support to remotely toggle push notifications ([\#2686](https://github.com/matrix-org/matrix-js-sdk/pull/2686)). + * Read receipts for threads ([\#2635](https://github.com/matrix-org/matrix-js-sdk/pull/2635)). + +## 🐛 Bug Fixes + * Unexpected ignored self key request when it's not shared history ([\#2724](https://github.com/matrix-org/matrix-js-sdk/pull/2724)). Contributed by @mcalinghee. + * Fix IDB initial migration handling causing spurious lazy loading upgrade loops ([\#2718](https://github.com/matrix-org/matrix-js-sdk/pull/2718)). Fixes vector-im/element-web#23377. + * Fix backpagination at end logic being spec non-conforming ([\#2680](https://github.com/matrix-org/matrix-js-sdk/pull/2680)). Fixes vector-im/element-web#22784. + Changes in [20.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.2) (2022-09-30) ================================================================================================== From 07476a0ae0cae47e078b086a820248bebca20cb5 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 4 Oct 2022 14:01:35 +0100 Subject: [PATCH 18/48] v20.1.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8395a2265..60588086f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "20.0.2", + "version": "20.1.0-rc.1", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=12.9.0" @@ -32,7 +32,7 @@ "keywords": [ "matrix-org" ], - "main": "./src/index.ts", + "main": "./lib/index.js", "browser": "./lib/browser-index.js", "matrix_src_main": "./src/index.ts", "matrix_src_browser": "./src/browser-index.js", @@ -126,5 +126,6 @@ "jestSonar": { "reportPath": "coverage", "sonar56x": true - } + }, + "typings": "./lib/index.d.ts" } From 2f24e90e53c7299ad085744e37a370d153a1c804 Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 4 Oct 2022 17:50:14 +0200 Subject: [PATCH 19/48] Device manager - `last_seen_user_agent` device property (#2728) * add last_seen_user_agent to IMyDevice type * add ubstable value * use unstable value in interface --- src/client.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/client.ts b/src/client.ts index ab86cf3b1..63b7aa1ac 100644 --- a/src/client.ts +++ b/src/client.ts @@ -215,6 +215,11 @@ export const CRYPTO_ENABLED: boolean = isCryptoAvailable(); const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes +export const UNSTABLE_MSC3852_LAST_SEEN_UA = new UnstableValue( + "last_seen_user_agent", + "org.matrix.msc3852.last_seen_user_agent", +); + interface IExportedDevice { olmDevice: IExportedOlmDevice; userId: string; @@ -688,6 +693,8 @@ export interface IMyDevice { display_name?: string; last_seen_ip?: string; last_seen_ts?: number; + [UNSTABLE_MSC3852_LAST_SEEN_UA.stable]?: string; + [UNSTABLE_MSC3852_LAST_SEEN_UA.unstable]?: string; } export interface IDownloadKeyResult { From b1ed9728676c8c84f711da7b344c8c1c395a4c57 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 4 Oct 2022 12:55:16 -0400 Subject: [PATCH 20/48] Use stable calls to `/room_keys` (#2729) * Use stable calls to `/room_keys` Fixes https://github.com/vector-im/element-web/issues/22839 * Appease the CI --- src/client.ts | 6 +++--- src/crypto/EncryptionSetup.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client.ts b/src/client.ts index 63b7aa1ac..d2273f5cb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2700,7 +2700,7 @@ export class MatrixClient extends TypedEventEmitter( undefined, Method.Get, "/room_keys/version", undefined, undefined, - { prefix: PREFIX_UNSTABLE }, + { prefix: PREFIX_V3 }, ); } catch (e) { if (e.errcode === 'M_NOT_FOUND') { @@ -2856,7 +2856,7 @@ export class MatrixClient extends TypedEventEmitter( undefined, Method.Post, "/room_keys/version", undefined, data, - { prefix: PREFIX_UNSTABLE }, + { prefix: PREFIX_V3 }, ); // We could assume everything's okay and enable directly, but this ensures @@ -2888,7 +2888,7 @@ export class MatrixClient extends TypedEventEmitter Date: Tue, 4 Oct 2022 13:00:45 -0400 Subject: [PATCH 21/48] Use the correct sender key when checking shared secret (#2730) --- src/crypto/SecretStorage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index aeb0d4596..9c5a9faf1 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -549,7 +549,7 @@ export class SecretStorage { const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey( olmlib.OLM_ALGORITHM, - content.sender_key, + event.getSenderKey() || "", ); if (senderKeyUser !== event.getSender()) { logger.error("sending device does not belong to the user it claims to be from"); From 04c1dfe43a716e0b7c29231b0f4b00f2c4a7c471 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Tue, 4 Oct 2022 18:27:40 +0100 Subject: [PATCH 22/48] Use the correct sender key when checking shared secret (#2730) (#2731) (cherry picked from commit 890a8406857f88fc071d0ea98496f0290ec80768) Co-authored-by: Hubert Chathi --- src/crypto/SecretStorage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index aeb0d4596..9c5a9faf1 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -549,7 +549,7 @@ export class SecretStorage { const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey( olmlib.OLM_ALGORITHM, - content.sender_key, + event.getSenderKey() || "", ); if (senderKeyUser !== event.getSender()) { logger.error("sending device does not belong to the user it claims to be from"); From 2935daeb3f1a09d8fd589d0577d9abeddbe37fee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Oct 2022 22:43:38 -0600 Subject: [PATCH 23/48] Update typescript-eslint monorepo to v5.39.0 (#2733) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 101 +++++++++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 47 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8965240fd..f164c0593 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1703,13 +1703,13 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.6.0": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz#9f05d42fa8fb9f62304cc2f5c2805e03c01c2620" - integrity sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ== + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.39.0.tgz#778b2d9e7f293502c7feeea6c74dca8eb3e67511" + integrity sha512-xVfKOkBm5iWMNGKQ2fwX5GVgBuHmZBO1tCRwXmY5oAIsPscfwm2UADDuNB8ZVYCtpQvJK4xpjrK7jEhcJ0zY9A== dependencies: - "@typescript-eslint/scope-manager" "5.38.1" - "@typescript-eslint/type-utils" "5.38.1" - "@typescript-eslint/utils" "5.38.1" + "@typescript-eslint/scope-manager" "5.39.0" + "@typescript-eslint/type-utils" "5.39.0" + "@typescript-eslint/utils" "5.39.0" debug "^4.3.4" ignore "^5.2.0" regexpp "^3.2.0" @@ -1717,69 +1717,69 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.6.0": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.38.1.tgz#c577f429f2c32071b92dff4af4f5fbbbd2414bd0" - integrity sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw== + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.39.0.tgz#93fa0bc980a3a501e081824f6097f7ca30aaa22b" + integrity sha512-PhxLjrZnHShe431sBAGHaNe6BDdxAASDySgsBCGxcBecVCi8NQWxQZMcizNA4g0pN51bBAn/FUfkWG3SDVcGlA== dependencies: - "@typescript-eslint/scope-manager" "5.38.1" - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/typescript-estree" "5.38.1" + "@typescript-eslint/scope-manager" "5.39.0" + "@typescript-eslint/types" "5.39.0" + "@typescript-eslint/typescript-estree" "5.39.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.38.1.tgz#f87b289ef8819b47189351814ad183e8801d5764" - integrity sha512-BfRDq5RidVU3RbqApKmS7RFMtkyWMM50qWnDAkKgQiezRtLKsoyRKIvz1Ok5ilRWeD9IuHvaidaLxvGx/2eqTQ== +"@typescript-eslint/scope-manager@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.39.0.tgz#873e1465afa3d6c78d8ed2da68aed266a08008d0" + integrity sha512-/I13vAqmG3dyqMVSZPjsbuNQlYS082Y7OMkwhCfLXYsmlI0ca4nkL7wJ/4gjX70LD4P8Hnw1JywUVVAwepURBw== dependencies: - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/visitor-keys" "5.38.1" + "@typescript-eslint/types" "5.39.0" + "@typescript-eslint/visitor-keys" "5.39.0" -"@typescript-eslint/type-utils@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.38.1.tgz#7f038fcfcc4ade4ea76c7c69b2aa25e6b261f4c1" - integrity sha512-UU3j43TM66gYtzo15ivK2ZFoDFKKP0k03MItzLdq0zV92CeGCXRfXlfQX5ILdd4/DSpHkSjIgLLLh1NtkOJOAw== +"@typescript-eslint/type-utils@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.39.0.tgz#0a8c00f95dce4335832ad2dc6bc431c14e32a0a6" + integrity sha512-KJHJkOothljQWzR3t/GunL0TPKY+fGJtnpl+pX+sJ0YiKTz3q2Zr87SGTmFqsCMFrLt5E0+o+S6eQY0FAXj9uA== dependencies: - "@typescript-eslint/typescript-estree" "5.38.1" - "@typescript-eslint/utils" "5.38.1" + "@typescript-eslint/typescript-estree" "5.39.0" + "@typescript-eslint/utils" "5.39.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.38.1.tgz#74f9d6dcb8dc7c58c51e9fbc6653ded39e2e225c" - integrity sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg== +"@typescript-eslint/types@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.39.0.tgz#f4e9f207ebb4579fd854b25c0bf64433bb5ed78d" + integrity sha512-gQMZrnfEBFXK38hYqt8Lkwt8f4U6yq+2H5VDSgP/qiTzC8Nw8JO3OuSUOQ2qW37S/dlwdkHDntkZM6SQhKyPhw== -"@typescript-eslint/typescript-estree@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.1.tgz#657d858d5d6087f96b638ee383ee1cff52605a1e" - integrity sha512-99b5e/Enoe8fKMLdSuwrfH/C0EIbpUWmeEKHmQlGZb8msY33qn1KlkFww0z26o5Omx7EVjzVDCWEfrfCDHfE7g== +"@typescript-eslint/typescript-estree@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.39.0.tgz#c0316aa04a1a1f4f7f9498e3c13ef1d3dc4cf88b" + integrity sha512-qLFQP0f398sdnogJoLtd43pUgB18Q50QSA+BTE5h3sUxySzbWDpTSdgt4UyxNSozY/oDK2ta6HVAzvGgq8JYnA== dependencies: - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/visitor-keys" "5.38.1" + "@typescript-eslint/types" "5.39.0" + "@typescript-eslint/visitor-keys" "5.39.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.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.38.1.tgz#e3ac37d7b33d1362bb5adf4acdbe00372fb813ef" - integrity sha512-oIuUiVxPBsndrN81oP8tXnFa/+EcZ03qLqPDfSZ5xIJVm7A9V0rlkQwwBOAGtrdN70ZKDlKv+l1BeT4eSFxwXA== +"@typescript-eslint/utils@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.39.0.tgz#b7063cca1dcf08d1d21b0d91db491161ad0be110" + integrity sha512-+DnY5jkpOpgj+EBtYPyHRjXampJfC0yUZZzfzLuUWVZvCuKqSdJVC8UhdWipIw7VKNTfwfAPiOWzYkAwuIhiAg== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.38.1" - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/typescript-estree" "5.38.1" + "@typescript-eslint/scope-manager" "5.39.0" + "@typescript-eslint/types" "5.39.0" + "@typescript-eslint/typescript-estree" "5.39.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.38.1": - version "5.38.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.1.tgz#508071bfc6b96d194c0afe6a65ad47029059edbc" - integrity sha512-bSHr1rRxXt54+j2n4k54p4fj8AHJ49VDWtjpImOpzQj4qjAiOpPni+V1Tyajh19Api1i844F757cur8wH3YvOA== +"@typescript-eslint/visitor-keys@5.39.0": + version "5.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.39.0.tgz#8f41f7d241b47257b081ddba5d3ce80deaae61e2" + integrity sha512-yyE3RPwOG+XJBLrhvsxAidUgybJVQ/hG8BhiJo0k8JSAYfk/CshVcxf0HwP4Jt7WZZ6vLmxdo1p6EyN3tzFTkg== dependencies: - "@typescript-eslint/types" "5.38.1" + "@typescript-eslint/types" "5.39.0" eslint-visitor-keys "^3.3.0" JSONStream@^1.0.3: @@ -6041,13 +6041,20 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.5, semver@^7.3.7: +semver@^7.3.5: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" +semver@^7.3.7: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" From ff720e3aa33fb432d8a1236fd7fd163ceb8d7b89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Oct 2022 22:43:50 -0600 Subject: [PATCH 24/48] Update all (#2732) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 8395a2265..9e74d0143 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "better-docs": "^2.4.0-beta.9", "browserify": "^17.0.0", "docdash": "^1.2.0", - "eslint": "8.23.1", + "eslint": "8.24.0", "eslint-config-google": "^0.14.0", "eslint-import-resolver-typescript": "^3.5.1", "eslint-plugin-import": "^2.26.0", diff --git a/yarn.lock b/yarn.lock index f164c0593..6208b71a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1049,7 +1049,7 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@humanwhocodes/config-array@^0.10.4": +"@humanwhocodes/config-array@^0.10.5": version "0.10.7" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.7.tgz#6d53769fd0c222767e6452e8ebda825c22e9f0dc" integrity sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w== @@ -1651,14 +1651,14 @@ integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== "@types/node@*": - version "18.8.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.0.tgz#b8ee8d83a99470c0661bd899417fcd77060682fe" - integrity sha512-u+h43R6U8xXDt2vzUaVP3VwjjLyOJk6uEciZS8OSyziUQGOwmk+l+4drxcsDboHXwyTaqS1INebghmWMRxq3LA== + version "18.8.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.2.tgz#17d42c6322d917764dd3d2d3a10d7884925de067" + integrity sha512-cRMwIgdDN43GO4xMWAfJAecYn8wV4JbsOGHNfNUIDiuYkUYAR5ec4Rj7IO2SAhFPEfpPtLtUTbbny/TCT7aDwA== "@types/node@16": - version "16.11.63" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.63.tgz#af57f6d2c3fb17a571230d527003dd734a9109b2" - integrity sha512-3OxnrEQLBz8EIIaHpg3CibmTAEGkDBcHY4fL5cnBwg2vd2yvHrUDGWxK+MlYPeXWWIoJJW79dGtU+oeBr6166Q== + version "16.11.64" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.64.tgz#9171f327298b619e2c52238b120c19056415d820" + integrity sha512-z5hPTlVFzNwtJ2LNozTpJcD1Cu44c4LNuzaq1mwxmiHWQh2ULdR6Vjwo1UGldzRpzL0yUEdZddnfqGW2G70z6Q== "@types/prettier@^2.1.5": version "2.7.1" @@ -3273,13 +3273,13 @@ 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.23.1: - version "8.23.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.23.1.tgz#cfd7b3f7fdd07db8d16b4ac0516a29c8d8dca5dc" - integrity sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg== +eslint@8.24.0: + version "8.24.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.24.0.tgz#489516c927a5da11b3979dbfb2679394523383c8" + integrity sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ== dependencies: "@eslint/eslintrc" "^1.3.2" - "@humanwhocodes/config-array" "^0.10.4" + "@humanwhocodes/config-array" "^0.10.5" "@humanwhocodes/gitignore-to-minimatch" "^1.0.2" "@humanwhocodes/module-importer" "^1.0.1" ajv "^6.10.0" @@ -5010,9 +5010,9 @@ matrix-events-sdk@^0.0.1-beta.7: integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== matrix-mock-request@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.1.2.tgz#11e38ed1233dced88a6f2bfba1684d5c5b3aa2c2" - integrity sha512-/OXCIzDGSLPJ3fs+uzDrtaOHI/Sqp4iEuniRn31U8S06mPXbvAnXknHqJ4c6A/KVwJj/nPFbGXpK4wPM038I6A== + version "2.4.1" + resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.4.1.tgz#a9c7dbb6b466f582ba2ca21f17cf18ceb41c7657" + integrity sha512-QMNpKUeHS2RHovSKybUySFTXTJ11EQPkp3bgvEXmNqAc3TYM23gKYqgI288BoBDYwQrK3WJFT0d4bvMiNIS/vA== dependencies: expect "^28.1.0" From 21a6f61b7b7bccb899c2aa34e6c881b729524bcd Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 5 Oct 2022 10:37:45 +0100 Subject: [PATCH 25/48] Add support for unread thread notifications (#2726) --- package.json | 1 + spec/integ/matrix-client-syncing.spec.ts | 69 ++++++++++++++ spec/test-utils/client.ts | 94 +++++++++++++++++++ spec/unit/filter.spec.ts | 13 +++ spec/unit/notifications.spec.ts | 114 +++++++++++++++++++++++ spec/unit/room.spec.ts | 38 +++++++- spec/unit/sync-accumulator.spec.ts | 13 +++ src/@types/sync.ts | 26 ++++++ src/client.ts | 99 ++++++++++++++------ src/filter.ts | 12 ++- src/models/read-receipt.ts | 4 +- src/models/room.ts | 36 ++++++- src/sync-accumulator.ts | 16 +++- src/sync.ts | 28 ++++++ src/utils.ts | 1 - yarn.lock | 27 +++++- 16 files changed, 551 insertions(+), 40 deletions(-) create mode 100644 spec/test-utils/client.ts create mode 100644 spec/unit/notifications.spec.ts create mode 100644 src/@types/sync.ts diff --git a/package.json b/package.json index 9e74d0143..f845212fe 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "fake-indexeddb": "^4.0.0", "jest": "^29.0.0", "jest-localstorage-mock": "^2.4.6", + "jest-mock": "^27.5.1", "jest-sonar-reporter": "^2.0.0", "jsdoc": "^3.6.6", "matrix-mock-request": "^2.1.2", diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index e07e52c7b..f1c43c4f9 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -29,7 +29,9 @@ import { MatrixClient, ClientEvent, IndexedDBCryptoStore, + NotificationCountType, } from "../../src"; +import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync'; import * as utils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; @@ -1363,6 +1365,73 @@ describe("MatrixClient syncing", () => { }); }); + describe("unread notifications", () => { + const THREAD_ID = "$ThisIsARandomEventId"; + + const syncData = { + rooms: { + join: { + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello", + }), + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "world", + }), + ], + }, + state: { + events: [ + utils.mkEvent({ + type: "m.room.name", room: roomOne, user: otherUserId, + content: { + name: "Room name", + }, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: otherUserId, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: selfUserId, + }), + utils.mkEvent({ + type: "m.room.create", room: roomOne, user: selfUserId, + content: { + creator: selfUserId, + }, + }), + ], + }, + }, + }, + }, + }; + it("should sync unread notifications.", () => { + syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = { + [THREAD_ID]: { + "highlight_count": 2, + "notification_count": 5, + }, + }; + + httpBackend!.when("GET", "/sync").respond(200, syncData); + + client!.startClient(); + + return Promise.all([ + httpBackend!.flushAllExpected(), + awaitSyncEvent(), + ]).then(() => { + const room = client!.getRoom(roomOne); + + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5); + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2); + }); + }); + }); + describe("of a room", () => { xit("should sync when a join event (which changes state) for the user" + " arrives down the event stream (e.g. join from another device)", () => { diff --git a/spec/test-utils/client.ts b/spec/test-utils/client.ts new file mode 100644 index 000000000..3cacd179d --- /dev/null +++ b/spec/test-utils/client.ts @@ -0,0 +1,94 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MethodKeysOf, mocked, MockedObject } from "jest-mock"; + +import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client"; +import { TypedEventEmitter } from "../../src/models/typed-event-emitter"; +import { User } from "../../src/models/user"; + +/** + * Mock client with real event emitter + * useful for testing code that listens + * to MatrixClient events + */ +export class MockClientWithEventEmitter extends TypedEventEmitter { + constructor(mockProperties: Partial, unknown>> = {}) { + super(); + Object.assign(this, mockProperties); + } +} + +/** + * - make a mock client + * - cast the type to mocked(MatrixClient) + * - spy on MatrixClientPeg.get to return the mock + * eg + * ``` + * const mockClient = getMockClientWithEventEmitter({ + getUserId: jest.fn().mockReturnValue(aliceId), + }); + * ``` + */ +export const getMockClientWithEventEmitter = ( + mockProperties: Partial, unknown>>, +): MockedObject => { + const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient); + return mock; +}; + +/** + * Returns basic mocked client methods related to the current user + * ``` + * const mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser('@mytestuser:domain'), + }); + * ``` + */ +export const mockClientMethodsUser = (userId = '@alice:domain') => ({ + getUserId: jest.fn().mockReturnValue(userId), + getUser: jest.fn().mockReturnValue(new User(userId)), + isGuest: jest.fn().mockReturnValue(false), + mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'), + credentials: { userId }, + getThreePids: jest.fn().mockResolvedValue({ threepids: [] }), + getAccessToken: jest.fn(), +}); + +/** + * Returns basic mocked client methods related to rendering events + * ``` + * const mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser('@mytestuser:domain'), + }); + * ``` + */ +export const mockClientMethodsEvents = () => ({ + decryptEventIfNeeded: jest.fn(), + getPushActionsForEvent: jest.fn(), +}); + +/** + * Returns basic mocked client methods related to server support + */ +export const mockClientMethodsServer = (): Partial, unknown>> => ({ + doesServerSupportSeparateAddAndBind: jest.fn(), + getIdentityServerUrl: jest.fn(), + getHomeserverUrl: jest.fn(), + getCapabilities: jest.fn().mockReturnValue({}), + doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false), +}); + diff --git a/spec/unit/filter.spec.ts b/spec/unit/filter.spec.ts index faa0f53ca..925915729 100644 --- a/spec/unit/filter.spec.ts +++ b/spec/unit/filter.spec.ts @@ -43,4 +43,17 @@ describe("Filter", function() { expect(filter.getDefinition()).toEqual(definition); }); }); + + describe("setUnreadThreadNotifications", function() { + it("setUnreadThreadNotifications", function() { + filter.setUnreadThreadNotifications(true); + expect(filter.getDefinition()).toEqual({ + room: { + timeline: { + unread_thread_notifications: true, + }, + }, + }); + }); + }); }); diff --git a/spec/unit/notifications.spec.ts b/spec/unit/notifications.spec.ts new file mode 100644 index 000000000..89601327b --- /dev/null +++ b/spec/unit/notifications.spec.ts @@ -0,0 +1,114 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { + EventType, + fixNotificationCountOnDecryption, + MatrixClient, + MatrixEvent, + MsgType, + NotificationCountType, + RelationType, + Room, +} from "../../src/matrix"; +import { IActionsObject } from "../../src/pushprocessor"; +import { ReEmitter } from "../../src/ReEmitter"; +import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client"; +import { mkEvent, mock } from "../test-utils/test-utils"; + +let mockClient: MatrixClient; +let room: Room; +let event: MatrixEvent; +let threadEvent: MatrixEvent; + +const ROOM_ID = "!roomId:example.org"; +let THREAD_ID; + +function mkPushAction(notify, highlight): IActionsObject { + return { + notify, + tweaks: { + highlight, + }, + }; +} + +describe("fixNotificationCountOnDecryption", () => { + beforeEach(() => { + mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(), + getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)), + getRoom: jest.fn().mockImplementation(() => room), + decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0), + supportsExperimentalThreads: jest.fn().mockReturnValue(true), + }); + mockClient.reEmitter = mock(ReEmitter, 'ReEmitter'); + + room = new Room(ROOM_ID, mockClient, mockClient.getUserId()); + room.setUnreadNotificationCount(NotificationCountType.Total, 1); + room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); + + event = mkEvent({ + type: EventType.RoomMessage, + content: { + msgtype: MsgType.Text, + body: "Hello world!", + }, + event: true, + }, mockClient); + + THREAD_ID = event.getId(); + threadEvent = mkEvent({ + type: EventType.RoomMessage, + content: { + "m.relates_to": { + rel_type: RelationType.Thread, + event_id: THREAD_ID, + }, + "msgtype": MsgType.Text, + "body": "Thread reply", + }, + event: true, + }); + room.createThread(THREAD_ID, event, [threadEvent], false); + + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1); + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0); + + event.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false)); + threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false)); + }); + + it("changes the room count to highlight on decryption", () => { + expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(1); + expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0); + + fixNotificationCountOnDecryption(mockClient, event); + + expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(1); + expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1); + }); + + it("changes the thread count to highlight on decryption", () => { + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1); + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0); + + fixNotificationCountOnDecryption(mockClient, threadEvent); + + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1); + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1); + }); +}); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index e79fb7110..d6891f369 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -32,7 +32,7 @@ import { RoomEvent, } from "../../src"; import { EventTimeline } from "../../src/models/event-timeline"; -import { Room } from "../../src/models/room"; +import { NotificationCountType, Room } from "../../src/models/room"; import { RoomState } from "../../src/models/room-state"; import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; @@ -2562,4 +2562,40 @@ describe("Room", function() { expect(client.roomNameGenerator).toHaveBeenCalled(); }); }); + + describe("thread notifications", () => { + let room; + + beforeEach(() => { + const client = new TestClient(userA).client; + room = new Room(roomId, client, userA); + }); + + it("defaults to undefined", () => { + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBeUndefined(); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined(); + }); + + it("lets you set values", () => { + room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1); + + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined(); + + room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 10); + + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(10); + }); + + it("lets you reset threads notifications", () => { + room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666); + room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123); + + room.resetThreadUnreadNotificationCount(); + + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBeUndefined(); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined(); + }); + }); }); diff --git a/spec/unit/sync-accumulator.spec.ts b/spec/unit/sync-accumulator.spec.ts index 645efbfbb..5618dbe22 100644 --- a/spec/unit/sync-accumulator.spec.ts +++ b/spec/unit/sync-accumulator.spec.ts @@ -30,6 +30,12 @@ const RES_WITH_AGE = { account_data: { events: [] }, ephemeral: { events: [] }, unread_notifications: {}, + unread_thread_notifications: { + "$143273582443PhrSn:example.org": { + highlight_count: 0, + notification_count: 1, + }, + }, timeline: { events: [ Object.freeze({ @@ -439,6 +445,13 @@ describe("SyncAccumulator", function() { Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]), ); }); + + it("should retrieve unread thread notifications", () => { + sa.accumulate(RES_WITH_AGE); + const output = sa.getJSON(); + expect(output.roomsData.join["!foo:bar"] + .unread_thread_notifications["$143273582443PhrSn:example.org"]).not.toBeUndefined(); + }); }); }); diff --git a/src/@types/sync.ts b/src/@types/sync.ts new file mode 100644 index 000000000..f25bbf2e4 --- /dev/null +++ b/src/@types/sync.ts @@ -0,0 +1,26 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { UnstableValue } from "matrix-events-sdk/lib/NamespacedValue"; + +/** + * https://github.com/matrix-org/matrix-doc/pull/3773 + * + * @experimental + */ +export const UNREAD_THREAD_NOTIFICATIONS = new UnstableValue( + "unread_thread_notifications", + "org.matrix.msc3773.unread_thread_notifications"); diff --git a/src/client.ts b/src/client.ts index d2273f5cb..ed3441f17 100644 --- a/src/client.ts +++ b/src/client.ts @@ -872,7 +872,7 @@ type UserEvents = UserEvent.AvatarUrl | UserEvent.CurrentlyActive | UserEvent.LastPresenceTs; -type EmittedEvents = ClientEvent +export type EmittedEvents = ClientEvent | RoomEvents | RoomStateEvents | CryptoEvents @@ -1088,35 +1088,7 @@ export class MatrixClient extends TypedEventEmitter { - const oldActions = event.getPushActions(); - const actions = this.getPushActionsForEvent(event, true); - - const room = this.getRoom(event.getRoomId()); - if (!room) return; - - const currentCount = room.getUnreadNotificationCount(NotificationCountType.Highlight); - - // Ensure the unread counts are kept up to date if the event is encrypted - // We also want to make sure that the notification count goes up if we already - // have encrypted events to avoid other code from resetting 'highlight' to zero. - const oldHighlight = !!oldActions?.tweaks?.highlight; - const newHighlight = !!actions?.tweaks?.highlight; - if (oldHighlight !== newHighlight || currentCount > 0) { - // TODO: Handle mentions received while the client is offline - // See also https://github.com/vector-im/element-web/issues/9069 - if (!room.hasUserReadEvent(this.getUserId(), event.getId())) { - let newCount = currentCount; - if (newHighlight && !oldHighlight) newCount++; - if (!newHighlight && oldHighlight) newCount--; - room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount); - - // Fix 'Mentions Only' rooms from not having the right badge count - const totalCount = room.getUnreadNotificationCount(NotificationCountType.Total); - if (totalCount < newCount) { - room.setUnreadNotificationCount(NotificationCountType.Total, newCount); - } - } - } + fixNotificationCountOnDecryption(this, event); }); // Like above, we have to listen for read receipts from ourselves in order to @@ -9236,6 +9208,73 @@ export class MatrixClient extends TypedEventEmitter 0) { + // TODO: Handle mentions received while the client is offline + // See also https://github.com/vector-im/element-web/issues/9069 + const hasReadEvent = isThreadEvent + ? room.getThread(event.threadRootId).hasUserReadEvent(cli.getUserId(), event.getId()) + : room.hasUserReadEvent(cli.getUserId(), event.getId()); + + if (!hasReadEvent) { + let newCount = currentCount; + if (newHighlight && !oldHighlight) newCount++; + if (!newHighlight && oldHighlight) newCount--; + + if (isThreadEvent) { + room.setThreadUnreadNotificationCount( + event.threadRootId, + NotificationCountType.Highlight, + newCount, + ); + } else { + room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount); + } + + // Fix 'Mentions Only' rooms from not having the right badge count + const totalCount = (isThreadEvent + ? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total) + : room.getUnreadNotificationCount(NotificationCountType.Total)) ?? 0; + + if (totalCount < newCount) { + if (isThreadEvent) { + room.setThreadUnreadNotificationCount( + event.threadRootId, + NotificationCountType.Total, + newCount, + ); + } else { + room.setUnreadNotificationCount(NotificationCountType.Total, newCount); + } + } + } + } +} + /** * Fires whenever the SDK receives a new event. *

diff --git a/src/filter.ts b/src/filter.ts index 663ba1bb9..0cf2d1c99 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -57,6 +57,8 @@ export interface IRoomEventFilter extends IFilterComponent { types?: Array; related_by_senders?: Array; related_by_rel_types?: string[]; + unread_thread_notifications?: boolean; + "org.matrix.msc3773.unread_thread_notifications"?: boolean; // Unstable values "io.element.relation_senders"?: Array; @@ -220,7 +222,15 @@ export class Filter { setProp(this.definition, "room.timeline.limit", limit); } - setLazyLoadMembers(enabled: boolean) { + /** + * Enable threads unread notification + * @param {boolean} enabled + */ + public setUnreadThreadNotifications(enabled: boolean): void { + setProp(this.definition, "room.timeline.unread_thread_notifications", !!enabled); + } + + setLazyLoadMembers(enabled: boolean): void { setProp(this.definition, "room.state.lazy_load_members", !!enabled); } diff --git a/src/models/read-receipt.ts b/src/models/read-receipt.ts index 1f7f5726f..e6d558766 100644 --- a/src/models/read-receipt.ts +++ b/src/models/read-receipt.ts @@ -282,7 +282,7 @@ export abstract class ReadReceipt< const readUpToId = this.getEventReadUpTo(userId, false); if (readUpToId === eventId) return true; - if (this.timeline.length + if (this.timeline?.length && this.timeline[this.timeline.length - 1].getSender() && this.timeline[this.timeline.length - 1].getSender() === userId) { // It doesn't matter where the event is in the timeline, the user has read @@ -290,7 +290,7 @@ export abstract class ReadReceipt< return true; } - for (let i = this.timeline.length - 1; i >= 0; --i) { + for (let i = this.timeline?.length - 1; i >= 0; --i) { const ev = this.timeline[i]; // If we encounter the target event first, the user hasn't read it diff --git a/src/models/room.ts b/src/models/room.ts index 75267fa2a..c8a31f00d 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -96,6 +96,8 @@ export interface IRecommendedVersion { // price to pay to keep matrix-js-sdk responsive. const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30; +type NotificationCount = Partial>; + export enum NotificationCountType { Highlight = "highlight", Total = "total", @@ -183,7 +185,8 @@ export type RoomEventHandlerMap = { export class Room extends ReadReceipt { public readonly reEmitter: TypedReEmitter; private txnToEvent: Record = {}; // Pending in-flight requests { string: MatrixEvent } - private notificationCounts: Partial> = {}; + private notificationCounts: NotificationCount = {}; + private threadNotifications: Map = new Map(); private readonly timelineSets: EventTimelineSet[]; public readonly threadsTimelineSets: EventTimelineSet[] = []; // any filtered timeline sets we're maintaining for this room @@ -1180,6 +1183,37 @@ export class Room extends ReadReceipt { return this.notificationCounts[type]; } + /** + * 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' + * @returns The notification count, or undefined if there is no count + * for this type. + */ + public getThreadUnreadNotificationCount(threadId: string, type = NotificationCountType.Total): number | undefined { + return this.threadNotifications.get(threadId)?.[type]; + } + + /** + * 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' + * @returns {void} + */ + public setThreadUnreadNotificationCount(threadId: string, type: NotificationCountType, count: number): void { + this.threadNotifications.set(threadId, { + highlight: this.threadNotifications.get(threadId)?.highlight, + total: this.threadNotifications.get(threadId)?.total, + ...{ + [type]: count, + }, + }); + } + + public resetThreadUnreadNotificationCount(): void { + this.threadNotifications.clear(); + } + /** * Set one of the notification counts for this room * @param {String} type The type of notification count to set. diff --git a/src/sync-accumulator.ts b/src/sync-accumulator.ts index 037c9231b..ec60c1c3a 100644 --- a/src/sync-accumulator.ts +++ b/src/sync-accumulator.ts @@ -25,6 +25,7 @@ import { IContent, IUnsigned } from "./models/event"; import { IRoomSummary } from "./models/room-summary"; import { EventType } from "./@types/event"; import { ReceiptType } from "./@types/read_receipts"; +import { UNREAD_THREAD_NOTIFICATIONS } from './@types/sync'; interface IOpts { maxTimelineEntries?: number; @@ -41,7 +42,7 @@ export interface IEphemeral { } /* eslint-disable camelcase */ -interface IUnreadNotificationCounts { +interface UnreadNotificationCounts { highlight_count?: number; notification_count?: number; } @@ -75,7 +76,9 @@ export interface IJoinedRoom { timeline: ITimeline; ephemeral: IEphemeral; account_data: IAccountData; - unread_notifications: IUnreadNotificationCounts; + unread_notifications: UnreadNotificationCounts; + unread_thread_notifications?: Record; + "org.matrix.msc3773.unread_thread_notifications"?: Record; } export interface IStrippedState { @@ -153,7 +156,8 @@ interface IRoom { }[]; _summary: Partial; _accountData: { [eventType: string]: IMinimalEvent }; - _unreadNotifications: Partial; + _unreadNotifications: Partial; + _unreadThreadNotifications?: Record>; _readReceipts: { [userId: string]: { data: IMinimalEvent; @@ -362,6 +366,7 @@ export class SyncAccumulator { _timeline: [], _accountData: Object.create(null), _unreadNotifications: {}, + _unreadThreadNotifications: {}, _summary: {}, _readReceipts: {}, }; @@ -379,6 +384,10 @@ export class SyncAccumulator { if (data.unread_notifications) { currentData._unreadNotifications = data.unread_notifications; } + currentData._unreadThreadNotifications = data[UNREAD_THREAD_NOTIFICATIONS.stable] + ?? data[UNREAD_THREAD_NOTIFICATIONS.unstable] + ?? undefined; + if (data.summary) { const HEROES_KEY = "m.heroes"; const INVITED_COUNT_KEY = "m.invited_member_count"; @@ -537,6 +546,7 @@ export class SyncAccumulator { prev_batch: null, }, unread_notifications: roomData._unreadNotifications, + unread_thread_notifications: roomData._unreadThreadNotifications, summary: roomData._summary as IRoomSummary, }; // Add account data diff --git a/src/sync.ts b/src/sync.ts index 768547949..0026831d5 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -58,6 +58,7 @@ import { RoomMemberEvent } from "./models/room-member"; import { BeaconEvent } from "./models/beacon"; import { IEventsResponse } from "./@types/requests"; import { IAbortablePromise } from "./@types/partials"; +import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; const DEBUG = true; @@ -705,6 +706,10 @@ export class SyncApi { const initialFilter = this.buildDefaultFilter(); initialFilter.setDefinition(filter.getDefinition()); initialFilter.setTimelineLimit(this.opts.initialSyncLimit); + const supportsThreadNotifications = + await this.client.doesServerSupportUnstableFeature("org.matrix.msc3773") + || await this.client.isVersionSupported("v1.4"); + initialFilter.setUnreadThreadNotifications(supportsThreadNotifications); // Use an inline filter, no point uploading it for a single usage firstSyncFilter = JSON.stringify(initialFilter.getDefinition()); } @@ -1264,6 +1269,29 @@ export class SyncApi { } } + room.resetThreadUnreadNotificationCount(); + const unreadThreadNotifications = joinObj[UNREAD_THREAD_NOTIFICATIONS.name] + ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName]; + if (unreadThreadNotifications) { + Object.entries(unreadThreadNotifications).forEach(([threadId, unreadNotification]) => { + room.setThreadUnreadNotificationCount( + threadId, + NotificationCountType.Total, + unreadNotification.notification_count, + ); + + const hasNoNotifications = + room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) <= 0; + if (!encrypted || (encrypted && hasNoNotifications)) { + room.setThreadUnreadNotificationCount( + threadId, + NotificationCountType.Highlight, + unreadNotification.highlight_count, + ); + } + }); + } + joinObj.timeline = joinObj.timeline || {} as ITimeline; if (joinObj.isBrandNewRoom) { diff --git a/src/utils.ts b/src/utils.ts index 2875cf3cf..818da7f64 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -673,4 +673,3 @@ export function sortEventsByLatestContentTimestamp(left: MatrixEvent, right: Mat export function isSupportedReceiptType(receiptType: string): boolean { return [ReceiptType.Read, ReceiptType.ReadPrivate].includes(receiptType as ReceiptType); } - diff --git a/yarn.lock b/yarn.lock index 6208b71a7..3f7f511b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1284,6 +1284,17 @@ slash "^3.0.0" write-file-atomic "^4.0.1" +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + "@jest/types@^28.1.3": version "28.1.3" resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.3.tgz#b05de80996ff12512bc5ceb1d208285a7d11748b" @@ -1358,7 +1369,6 @@ "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz": version "3.2.12" - uid "0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz#0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9" "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": @@ -1695,6 +1705,13 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^17.0.8": version "17.0.13" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76" @@ -4471,6 +4488,14 @@ jest-message-util@^29.1.2: slash "^3.0.0" stack-utils "^2.0.3" +jest-mock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" + integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock@^29.1.2: version "29.1.2" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.1.2.tgz#de47807edbb9d4abf8423f1d8d308d670105678c" From a7aa227f553e650c68ad91620290fc1ede133630 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 5 Oct 2022 13:23:42 +0100 Subject: [PATCH 26/48] Prepare changelog for v20.1.0-rc.2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35c5d440c..2cb01fff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [20.1.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0-rc.2) (2022-10-05) +============================================================================================================ + +## 🐛 Bug Fixes + * Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374. + Changes in [20.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0-rc.1) (2022-10-04) ============================================================================================================ From 121250a6fb7709de8fb49cc2c40d0ad11f09365a Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 5 Oct 2022 13:23:42 +0100 Subject: [PATCH 27/48] v20.1.0-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 60588086f..ab720d111 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "20.1.0-rc.1", + "version": "20.1.0-rc.2", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=12.9.0" From 3a3dcfb2540fbb16e259afde389abb58f24d7bbe Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 23:10:42 +0200 Subject: [PATCH 28/48] Load Thread List with server-side assistance (MSC3856) (#2602) * feature detection code for thread list api * fix bug where createThreadsTimelineSets would sometimes return nothing * initial implementation of thread listing msc * tests for thread list pagination --- .../matrix-client-event-timeline.spec.ts | 350 +++++++++++++++++- spec/unit/room.spec.ts | 4 +- src/client.ts | 216 ++++++++--- src/models/event-timeline-set.ts | 3 + src/models/event-timeline.ts | 4 +- src/models/room-state.ts | 2 +- src/models/room.ts | 185 ++++++--- src/models/thread.ts | 33 +- 8 files changed, 680 insertions(+), 117 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index c656f6447..8640e82be 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -15,10 +15,21 @@ limitations under the License. */ import * as utils from "../test-utils/test-utils"; -import { ClientEvent, EventTimeline, Filter, IEvent, MatrixClient, MatrixEvent, Room } from "../../src/matrix"; +import { + ClientEvent, + Direction, + EventTimeline, + EventTimelineSet, + Filter, + IEvent, + MatrixClient, + MatrixEvent, + Room, +} from "../../src/matrix"; import { logger } from "../../src/logger"; +import { encodeUri } from "../../src/utils"; import { TestClient } from "../TestClient"; -import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread"; +import { FeatureSupport, Thread, THREAD_RELATION_TYPE } from "../../src/models/thread"; const userId = "@alice:localhost"; const userName = "Alice"; @@ -145,8 +156,11 @@ SYNC_THREAD_ROOT.unsigned = { }, }; +type HttpBackend = TestClient["httpBackend"]; +type ExpectedHttpRequest = ReturnType; + // start the client, and wait for it to initialise -function startClient(httpBackend: TestClient["httpBackend"], client: MatrixClient) { +function startClient(httpBackend: HttpBackend, client: MatrixClient) { httpBackend.when("GET", "/versions").respond(200, {}); httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); @@ -172,7 +186,7 @@ function startClient(httpBackend: TestClient["httpBackend"], client: MatrixClien } describe("getEventTimeline support", function() { - let httpBackend: TestClient["httpBackend"]; + let httpBackend: HttpBackend; let client: MatrixClient; beforeEach(function() { @@ -189,6 +203,16 @@ describe("getEventTimeline support", function() { }); it("timeline support must be enabled to work", function() { + const testClient = new TestClient( + userId, + "DEVICE", + accessToken, + undefined, + { timelineSupport: false }, + ); + client = testClient.client; + httpBackend = testClient.httpBackend; + return startClient(httpBackend, client).then(function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; @@ -214,6 +238,23 @@ describe("getEventTimeline support", function() { }); }); + it("only works with room timelines", function() { + const testClient = new TestClient( + userId, + "DEVICE", + accessToken, + undefined, + { timelineSupport: true }, + ); + client = testClient.client; + httpBackend = testClient.httpBackend; + + return startClient(httpBackend, client).then(function() { + const timelineSet = new EventTimelineSet(undefined); + expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy(); + }); + }); + it("scrollback should be able to scroll back to before a gappy /sync", function() { // need a client with timelineSupport disabled to make this work let room: Room; @@ -280,7 +321,7 @@ describe("getEventTimeline support", function() { describe("MatrixClient event timelines", function() { let client: MatrixClient; - let httpBackend: TestClient["httpBackend"]; + let httpBackend: HttpBackend; beforeEach(function() { const testClient = new TestClient( @@ -299,7 +340,7 @@ describe("MatrixClient event timelines", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); client.stopClient(); - Thread.setServerSideSupport(false, false); + Thread.setServerSideSupport(FeatureSupport.None); }); describe("getEventTimeline", function() { @@ -552,7 +593,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; - Thread.setServerSideSupport(true, false); + Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); const thread = room.createThread(THREAD_ROOT.event_id, undefined, [], false); @@ -598,7 +639,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; - Thread.setServerSideSupport(true, false); + Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); const threadRoot = new MatrixEvent(THREAD_ROOT); @@ -630,7 +671,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; - Thread.setServerSideSupport(true, false); + Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); const threadRoot = new MatrixEvent(THREAD_ROOT); @@ -658,7 +699,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; - Thread.setServerSideSupport(true, false); + Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; @@ -711,6 +752,63 @@ describe("MatrixClient event timelines", function() { }); describe("getLatestTimeline", function() { + it("timeline support must be enabled to work", async function() { + await client.stopClient(); + + const testClient = new TestClient( + userId, + "DEVICE", + accessToken, + undefined, + { timelineSupport: false }, + ); + client = testClient.client; + httpBackend = testClient.httpBackend; + await startClient(httpBackend, client); + + const room = client.getRoom(roomId); + const timelineSet = room.getTimelineSets()[0]; + await expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy(); + }); + + it("timeline support works when enabled", async function() { + await client.stopClient(); + + const testClient = new TestClient( + userId, + "DEVICE", + accessToken, + undefined, + { timelineSupport: true }, + ); + client = testClient.client; + httpBackend = testClient.httpBackend; + + return startClient(httpBackend, client).then(() => { + const room = client.getRoom(roomId); + const timelineSet = room.getTimelineSets()[0]; + expect(client.getLatestTimeline(timelineSet)).rejects.toBeFalsy(); + }); + }); + + it("only works with room timelines", async function() { + await client.stopClient(); + + const testClient = new TestClient( + userId, + "DEVICE", + accessToken, + undefined, + { timelineSupport: true }, + ); + client = testClient.client; + httpBackend = testClient.httpBackend; + await startClient(httpBackend, client); + + const timelineSet = new EventTimelineSet(undefined); + await expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy(); + }); + it("should create a new timeline for new events", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; @@ -925,6 +1023,236 @@ describe("MatrixClient event timelines", function() { }); }); + describe("paginateEventTimeline for thread list timeline", function() { + async function flushHttp(promise: Promise): Promise { + return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result); + } + + const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; + + function respondToFilter(): ExpectedHttpRequest { + const request = httpBackend.when("POST", "/filter"); + request.respond(200, { filter_id: "fid" }); + return request; + } + + function respondToSync(): ExpectedHttpRequest { + const request = httpBackend.when("GET", "/sync"); + request.respond(200, INITIAL_SYNC_DATA); + return request; + } + + function respondToThreads( + response = { + chunk: [THREAD_ROOT], + state: [], + next_batch: RANDOM_TOKEN, + }, + ): ExpectedHttpRequest { + const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { + $roomId: roomId, + })); + request.respond(200, response); + return request; + } + + function respondToContext(): ExpectedHttpRequest { + const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", { + $roomId: roomId, + $eventId: THREAD_ROOT.event_id!, + })); + request.respond(200, { + end: `${Direction.Forward}${RANDOM_TOKEN}1`, + start: `${Direction.Backward}${RANDOM_TOKEN}1`, + state: [], + events_before: [], + events_after: [], + event: THREAD_ROOT, + }); + return request; + } + function respondToMessagesRequest(): ExpectedHttpRequest { + const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", { + $roomId: roomId, + })); + request.respond(200, { + chunk: [THREAD_ROOT], + state: [], + start: `${Direction.Forward}${RANDOM_TOKEN}2`, + end: `${Direction.Backward}${RANDOM_TOKEN}2`, + }); + return request; + } + + describe("with server compatibility", function() { + beforeEach(() => { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.Stable); + }); + + async function testPagination(timelineSet: EventTimelineSet, direction: Direction) { + respondToContext(); + await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!)); + respondToThreads(); + const timeline = await flushHttp(client.getLatestTimeline(timelineSet)); + expect(timeline).not.toBeNull(); + + respondToThreads(); + const success = await flushHttp(client.paginateEventTimeline(timeline!, { + backwards: direction === Direction.Backward, + })); + expect(success).toBeTruthy(); + expect(timeline!.getEvents().map(it => it.event)).toEqual([THREAD_ROOT]); + expect(timeline!.getPaginationToken(direction)).toEqual(RANDOM_TOKEN); + } + + it("should allow you to paginate all threads backwards", async function() { + const room = client.getRoom(roomId); + const timelineSets = await (room?.createThreadsTimelineSets()); + expect(timelineSets).not.toBeNull(); + const [allThreads, myThreads] = timelineSets!; + await testPagination(allThreads, Direction.Backward); + await testPagination(myThreads, Direction.Backward); + }); + + it("should allow you to paginate all threads forwards", async function() { + const room = client.getRoom(roomId); + const timelineSets = await (room?.createThreadsTimelineSets()); + expect(timelineSets).not.toBeNull(); + const [allThreads, myThreads] = timelineSets!; + + await testPagination(allThreads, Direction.Forward); + await testPagination(myThreads, Direction.Forward); + }); + + it("should allow fetching all threads", async function() { + const room = client.getRoom(roomId); + const timelineSets = await room?.createThreadsTimelineSets(); + expect(timelineSets).not.toBeNull(); + respondToThreads(); + respondToThreads(); + httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA); + await flushHttp(room.fetchRoomThreads()); + }); + }); + + describe("without server compatibility", function() { + beforeEach(() => { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.None); + }); + + async function testPagination(timelineSet: EventTimelineSet, direction: Direction) { + respondToContext(); + respondToSync(); + await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!)); + + respondToMessagesRequest(); + const timeline = await flushHttp(client.getLatestTimeline(timelineSet)); + expect(timeline).not.toBeNull(); + + respondToMessagesRequest(); + const success = await flushHttp(client.paginateEventTimeline(timeline!, { + backwards: direction === Direction.Backward, + })); + + expect(success).toBeTruthy(); + expect(timeline!.getEvents().map(it => it.event)).toEqual([THREAD_ROOT]); + expect(timeline!.getPaginationToken(direction)).toEqual(`${direction}${RANDOM_TOKEN}2`); + } + + it("should allow you to paginate all threads", async function() { + const room = client.getRoom(roomId); + + respondToFilter(); + respondToSync(); + respondToFilter(); + respondToSync(); + + const timelineSetsPromise = room?.createThreadsTimelineSets(); + expect(timelineSetsPromise).not.toBeNull(); + const timelineSets = await flushHttp(timelineSetsPromise!); + expect(timelineSets).not.toBeNull(); + const [allThreads, myThreads] = timelineSets!; + + await testPagination(allThreads, Direction.Backward); + await testPagination(myThreads, Direction.Backward); + }); + + it("should allow fetching all threads", async function() { + const room = client.getRoom(roomId); + + respondToFilter(); + respondToSync(); + respondToFilter(); + respondToSync(); + + const timelineSetsPromise = room?.createThreadsTimelineSets(); + expect(timelineSetsPromise).not.toBeNull(); + await flushHttp(timelineSetsPromise!); + respondToFilter(); + respondToSync(); + respondToSync(); + respondToSync(); + respondToMessagesRequest(); + await flushHttp(room.fetchRoomThreads()); + }); + }); + + it("should add lazy loading filter", async () => { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.Stable); + // @ts-ignore + client.clientOpts.lazyLoadMembers = true; + + const room = client.getRoom(roomId); + const timelineSets = await room?.createThreadsTimelineSets(); + expect(timelineSets).not.toBeNull(); + const [allThreads] = timelineSets!; + + respondToThreads().check((request) => { + expect(request.queryParams.filter).toEqual(JSON.stringify({ + "lazy_load_members": true, + })); + }); + + await flushHttp(client.paginateEventTimeline(allThreads.getLiveTimeline(), { + backwards: true, + })); + }); + + it("should correctly pass pagination token", async () => { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.Stable); + + const room = client.getRoom(roomId); + const timelineSets = await room?.createThreadsTimelineSets(); + expect(timelineSets).not.toBeNull(); + const [allThreads] = timelineSets!; + + respondToThreads({ + chunk: [THREAD_ROOT], + state: [], + next_batch: null, + }).check((request) => { + expect(request.queryParams.from).toEqual(RANDOM_TOKEN); + }); + + allThreads.getLiveTimeline().setPaginationToken(RANDOM_TOKEN, Direction.Backward); + await flushHttp(client.paginateEventTimeline(allThreads.getLiveTimeline(), { + backwards: true, + })); + }); + }); + describe("event timeline for sent events", function() { const TXN_ID = "txn1"; const event = utils.mkMessage({ @@ -1092,7 +1420,7 @@ describe("MatrixClient event timelines", function() { it("should re-insert room IDs for bundled thread relation events", async () => { // @ts-ignore client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(true, false); + Thread.setServerSideSupport(FeatureSupport.Experimental); httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index d6891f369..de9f0c5f9 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -38,7 +38,7 @@ import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; import { emitPromise } from "../test-utils/test-utils"; import { ReceiptType } from "../../src/@types/read_receipts"; -import { Thread, ThreadEvent } from "../../src/models/thread"; +import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread"; import { WrappedReceipt } from "../../src/models/read-receipt"; describe("Room", function() { @@ -2408,7 +2408,7 @@ describe("Room", function() { }); it("should aggregate relations in thread event timeline set", () => { - Thread.setServerSideSupport(true, true); + Thread.setServerSideSupport(FeatureSupport.Stable); const threadRoot = mkMessage(); const rootReaction = mkReaction(threadRoot); const threadResponse = mkThreadResponse(threadRoot); diff --git a/src/client.ts b/src/client.ts index ed3441f17..80296c472 100644 --- a/src/client.ts +++ b/src/client.ts @@ -19,7 +19,7 @@ limitations under the License. * @module client */ -import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent } from "matrix-events-sdk"; +import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk"; import { ISyncStateData, SyncApi, SyncState } from "./sync"; import { @@ -33,7 +33,7 @@ import { } from "./models/event"; import { StubStore } from "./store/stub"; import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call"; -import { Filter, IFilterDefinition } from "./filter"; +import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter"; import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler'; import * as utils from './utils'; import { sleep } from './utils'; @@ -195,7 +195,7 @@ import { TypedEventEmitter } from "./models/typed-event-emitter"; import { ReceiptType } from "./@types/read_receipts"; import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync"; import { SlidingSyncSdk } from "./sliding-sync-sdk"; -import { Thread, THREAD_RELATION_TYPE } from "./models/thread"; +import { FeatureSupport, Thread, THREAD_RELATION_TYPE, determineFeatureSupport } from "./models/thread"; import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon"; import { UnstableValue } from "./NamespacedValue"; import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue"; @@ -596,6 +596,13 @@ interface IMessagesResponse { state: IStateEvent[]; } +interface IThreadedMessagesResponse { + prev_batch: string; + next_batch: string; + chunk: IRoomEvent[]; + state: IStateEvent[]; +} + export interface IRequestTokenResponse { sid: string; submit_url?: string; @@ -1190,14 +1197,9 @@ export class MatrixClient extends TypedEventEmitter { + public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise> { // don't allow any timeline support unless it's been enabled. if (!this.timelineSupport) { throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable it."); } + if (!timelineSet?.room) { + throw new Error("getEventTimeline only supports room timelines"); + } + if (timelineSet.getTimelineForEvent(eventId)) { return timelineSet.getTimelineForEvent(eventId); } @@ -5345,7 +5351,7 @@ export class MatrixClient extends TypedEventEmitter = undefined; + let params: Record | undefined = undefined; if (this.clientOpts.lazyLoadMembers) { params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) }; } @@ -5438,27 +5444,36 @@ export class MatrixClient extends TypedEventEmitter { + public async getLatestTimeline(timelineSet: EventTimelineSet): Promise> { // don't allow any timeline support unless it's been enabled. if (!this.timelineSupport) { throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable it."); } - const messagesPath = utils.encodeUri( - "/rooms/$roomId/messages", { - $roomId: timelineSet.room.roomId, - }, - ); - - const params: Record = { - dir: 'b', - }; - if (this.clientOpts.lazyLoadMembers) { - params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER); + if (!timelineSet.room) { + throw new Error("getLatestTimeline only supports room timelines"); } - const res = await this.http.authedRequest(undefined, Method.Get, messagesPath, params); + let res: IMessagesResponse; + const roomId = timelineSet.room.roomId; + if (timelineSet.isThreadTimeline) { + res = await this.createThreadListMessagesRequest( + roomId, + null, + 1, + Direction.Backward, + timelineSet.getFilter(), + ); + } else { + res = await this.createMessagesRequest( + roomId, + null, + 1, + Direction.Backward, + timelineSet.getFilter(), + ); + } const event = res.chunk?.[0]; if (!event) { throw new Error("No message returned from /messages when trying to construct getLatestTimeline"); @@ -5498,7 +5513,7 @@ export class MatrixClient extends TypedEventEmitter { + const path = utils.encodeUri("/rooms/$roomId/threads", { $roomId: roomId }); + + const params: Record = { + limit: limit.toString(), + dir: dir, + include: 'all', + }; + + if (fromToken) { + params.from = fromToken; + } + + let filter: IRoomEventFilter | null = null; + if (this.clientOpts.lazyLoadMembers) { + // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, + // so the timelineFilter doesn't get written into it below + filter = { + ...filter, + ...Filter.LAZY_LOADING_MESSAGES_FILTER, + }; + } + if (timelineFilter) { + // XXX: it's horrific that /messages' filter parameter doesn't match + // /sync's one - see https://matrix.org/jira/browse/SPEC-451 + filter = { + ...filter, + ...timelineFilter.getRoomTimelineFilterComponent()?.toJSON(), + }; + } + if (filter) { + params.filter = JSON.stringify(filter); + } + + const opts: { prefix?: string } = {}; + if (Thread.hasServerSideListSupport === FeatureSupport.Experimental) { + opts.prefix = "/_matrix/client/unstable/org.matrix.msc3856"; + } + + return this.http.authedRequest(undefined, Method.Get, path, params, undefined, opts) + .then(res => ({ + ...res, + start: res.prev_batch, + end: res.next_batch, + })); + } + /** * Take an EventTimeline, and back/forward-fill results. * @@ -5531,6 +5612,8 @@ export class MatrixClient extends TypedEventEmitter { const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet); + const room = this.getRoom(eventTimeline.getRoomId()); + const isThreadTimeline = eventTimeline.getTimelineSet().isThreadTimeline; // TODO: we should implement a backoff (as per scrollback()) to deal more // nicely with HTTP errors. @@ -5564,7 +5647,7 @@ export class MatrixClient extends TypedEventEmitter { const token = res.next_token; - const matrixEvents = []; + const matrixEvents: MatrixEvent[] = []; for (let i = 0; i < res.notifications.length; i++) { const notification = res.notifications[i]; @@ -5596,13 +5679,48 @@ export class MatrixClient extends TypedEventEmitter { + eventTimeline.paginationRequests[dir] = null; + }); + eventTimeline.paginationRequests[dir] = promise; + } else if (isThreadTimeline) { + if (!room) { + throw new Error("Unknown room " + eventTimeline.getRoomId()); + } + + promise = this.createThreadListMessagesRequest( + eventTimeline.getRoomId(), + token, + opts.limit, + dir, + eventTimeline.getFilter(), + ).then((res) => { + if (res.state) { + const roomState = eventTimeline.getState(dir); + const stateEvents = res.state.map(this.getEventMapper()); + roomState.setUnknownStateEvents(stateEvents); + } + const token = res.end; + const matrixEvents = res.chunk.map(this.getEventMapper()); + + const timelineSet = eventTimeline.getTimelineSet(); + timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); + this.processBeaconEvents(room, matrixEvents); + this.processThreadRoots(room, matrixEvents, backwards); + + // 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 + // we can recover from gappy syncs. + if (backwards && res.end == res.start) { + eventTimeline.setPaginationToken(null, dir); + } + return res.end !== res.start; }).finally(() => { eventTimeline.paginationRequests[dir] = null; }); eventTimeline.paginationRequests[dir] = promise; } else { - const room = this.getRoom(eventTimeline.getRoomId()); if (!room) { throw new Error("Unknown room " + eventTimeline.getRoomId()); } @@ -5623,9 +5741,9 @@ export class MatrixClient extends TypedEventEmitter { + threads: FeatureSupport; + list: FeatureSupport; + }> { try { - const hasUnstableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440"); - const hasStableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"); + const [threadUnstable, threadStable, listUnstable, listStable] = await Promise.all([ + this.doesServerSupportUnstableFeature("org.matrix.msc3440"), + this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"), + this.doesServerSupportUnstableFeature("org.matrix.msc3856"), + this.doesServerSupportUnstableFeature("org.matrix.msc3856.stable"), + ]); // TODO: Use `this.isVersionSupported("v1.3")` for whatever spec version includes MSC3440 formally. return { - serverSupport: hasUnstableSupport || hasStableSupport, - stable: hasStableSupport, + threads: determineFeatureSupport(threadStable, threadUnstable), + list: determineFeatureSupport(listStable, listUnstable), }; } catch (e) { - // Assume server support and stability aren't available: null/no data return. - // XXX: This should just return an object with `false` booleans instead. - return null; + return { + threads: FeatureSupport.None, + list: FeatureSupport.None, + }; } } @@ -9160,6 +9283,13 @@ export class MatrixClient extends TypedEventEmitter> = { + public paginationRequests: Record | null> = { [Direction.Backward]: null, [Direction.Forward]: null, }; @@ -311,7 +311,7 @@ export class EventTimeline { * token for going backwards in time; EventTimeline.FORWARDS to set the * pagination token for going forwards in time. */ - public setPaginationToken(token: string, direction: Direction): void { + public setPaginationToken(token: string | null, direction: Direction): void { this.getState(direction).paginationToken = token; } diff --git a/src/models/room-state.ts b/src/models/room-state.ts index b0104cf70..3cca4a7b8 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -97,7 +97,7 @@ export class RoomState extends TypedEventEmitter // XXX: Should be read-only public members: Record = {}; // userId: RoomMember public events = new Map>(); // Map> - public paginationToken: string = null; + public paginationToken: string | null = null; public readonly beacons = new Map(); private _liveBeaconIds: BeaconIdentifier[] = []; diff --git a/src/models/room.ts b/src/models/room.ts index c8a31f00d..aa1ffdd74 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -359,7 +359,7 @@ export class Room extends ReadReceipt { } private threadTimelineSetsPromise: Promise<[EventTimelineSet, EventTimelineSet]> | null = null; - public async createThreadsTimelineSets(): Promise<[EventTimelineSet, EventTimelineSet]> { + public async createThreadsTimelineSets(): Promise<[EventTimelineSet, EventTimelineSet] | null> { if (this.threadTimelineSetsPromise) { return this.threadTimelineSetsPromise; } @@ -372,10 +372,13 @@ export class Room extends ReadReceipt { ]); const timelineSets = await this.threadTimelineSetsPromise; this.threadsTimelineSets.push(...timelineSets); + return timelineSets; } catch (e) { this.threadTimelineSetsPromise = null; + return null; } } + return null; } /** @@ -1612,7 +1615,14 @@ export class Room extends ReadReceipt { private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise { let timelineSet: EventTimelineSet; - if (Thread.hasServerSideSupport) { + if (Thread.hasServerSideListSupport) { + timelineSet = + new EventTimelineSet(this, this.opts, undefined, undefined, Boolean(Thread.hasServerSideListSupport)); + this.reEmitter.reEmit(timelineSet, [ + RoomEvent.Timeline, + RoomEvent.TimelineReset, + ]); + } else if (Thread.hasServerSideSupport) { const filter = await this.getThreadListFilter(filterType); timelineSet = this.getOrCreateFilteredTimelineSet( @@ -1645,81 +1655,148 @@ export class Room extends ReadReceipt { return timelineSet; } - public threadsReady = false; + private threadsReady = false; + /** + * Takes the given thread root events and creates threads for them. + * @param events + * @param toStartOfTimeline + */ + public processThreadRoots(events: MatrixEvent[], toStartOfTimeline: boolean): void { + for (const rootEvent of events) { + EventTimeline.setEventMetadata( + rootEvent, + this.currentState, + toStartOfTimeline, + ); + if (!this.getThread(rootEvent.getId())) { + this.createThread(rootEvent.getId(), rootEvent, [], toStartOfTimeline); + } + } + } + + /** + * Fetch the bare minimum of room threads required for the thread list to work reliably. + * With server support that means fetching one page. + * Without server support that means fetching as much at once as the server allows us to. + */ public async fetchRoomThreads(): Promise { if (this.threadsReady || !this.client.supportsExperimentalThreads()) { return; } - const allThreadsFilter = await this.getThreadListFilter(); + if (Thread.hasServerSideListSupport) { + await Promise.all([ + this.fetchRoomThreadList(ThreadFilterType.All), + this.fetchRoomThreadList(ThreadFilterType.My), + ]); + } else { + const allThreadsFilter = await this.getThreadListFilter(); - const { chunk: events } = await this.client.createMessagesRequest( - this.roomId, - "", - Number.MAX_SAFE_INTEGER, - Direction.Backward, - allThreadsFilter, - ); + const { chunk: events } = await this.client.createMessagesRequest( + this.roomId, + "", + Number.MAX_SAFE_INTEGER, + Direction.Backward, + allThreadsFilter, + ); - if (!events.length) return; + if (!events.length) return; - // Sorted by last_reply origin_server_ts - const threadRoots = events - .map(this.client.getEventMapper()) - .sort((eventA, eventB) => { - /** - * `origin_server_ts` in a decentralised world is far from ideal - * but for lack of any better, we will have to use this - * Long term the sorting should be handled by homeservers and this - * is only meant as a short term patch - */ - const threadAMetadata = eventA - .getServerAggregatedRelation(RelationType.Thread); - const threadBMetadata = eventB - .getServerAggregatedRelation(RelationType.Thread); - return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts; - }); + // Sorted by last_reply origin_server_ts + const threadRoots = events + .map(this.client.getEventMapper()) + .sort((eventA, eventB) => { + /** + * `origin_server_ts` in a decentralised world is far from ideal + * but for lack of any better, we will have to use this + * Long term the sorting should be handled by homeservers and this + * is only meant as a short term patch + */ + const threadAMetadata = eventA + .getServerAggregatedRelation(THREAD_RELATION_TYPE.name); + const threadBMetadata = eventB + .getServerAggregatedRelation(THREAD_RELATION_TYPE.name); + return threadAMetadata.latest_event.origin_server_ts - + threadBMetadata.latest_event.origin_server_ts; + }); - let latestMyThreadsRootEvent: MatrixEvent; - const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); - for (const rootEvent of threadRoots) { - this.threadsTimelineSets[0].addLiveEvent(rootEvent, { - duplicateStrategy: DuplicateStrategy.Ignore, - fromCache: false, - roomState, - }); - - const threadRelationship = rootEvent - .getServerAggregatedRelation(RelationType.Thread); - if (threadRelationship.current_user_participated) { - this.threadsTimelineSets[1].addLiveEvent(rootEvent, { + let latestMyThreadsRootEvent: MatrixEvent; + const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); + for (const rootEvent of threadRoots) { + this.threadsTimelineSets[0].addLiveEvent(rootEvent, { duplicateStrategy: DuplicateStrategy.Ignore, fromCache: false, roomState, }); - latestMyThreadsRootEvent = rootEvent; + + const threadRelationship = rootEvent + .getServerAggregatedRelation(THREAD_RELATION_TYPE.name); + if (threadRelationship.current_user_participated) { + this.threadsTimelineSets[1].addLiveEvent(rootEvent, { + duplicateStrategy: DuplicateStrategy.Ignore, + fromCache: false, + roomState, + }); + latestMyThreadsRootEvent = rootEvent; + } } - if (!this.getThread(rootEvent.getId())) { - this.createThread(rootEvent.getId(), rootEvent, [], true); + this.processThreadRoots(threadRoots, true); + + this.client.decryptEventIfNeeded(threadRoots[threadRoots.length -1]); + if (latestMyThreadsRootEvent) { + this.client.decryptEventIfNeeded(latestMyThreadsRootEvent); } } - this.client.decryptEventIfNeeded(threadRoots[threadRoots.length -1]); - if (latestMyThreadsRootEvent) { - this.client.decryptEventIfNeeded(latestMyThreadsRootEvent); - } - - this.threadsReady = true; - this.on(ThreadEvent.NewReply, this.onThreadNewReply); + this.threadsReady = true; + } + + /** + * Fetch a single page of threadlist messages for the specific thread filter + * @param filter + * @private + */ + private async fetchRoomThreadList(filter?: ThreadFilterType): Promise { + const timelineSet = filter === ThreadFilterType.My + ? this.threadsTimelineSets[1] + : this.threadsTimelineSets[0]; + + const { chunk: events, end } = await this.client.createThreadListMessagesRequest( + this.roomId, + null, + undefined, + Direction.Backward, + timelineSet.getFilter(), + ); + + timelineSet.getLiveTimeline().setPaginationToken(end, Direction.Backward); + + if (!events.length) return; + + const matrixEvents = events.map(this.client.getEventMapper()); + this.processThreadRoots(matrixEvents, true); + const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); + for (const rootEvent of matrixEvents) { + timelineSet.addLiveEvent(rootEvent, { + duplicateStrategy: DuplicateStrategy.Replace, + fromCache: false, + roomState, + }); + } } private onThreadNewReply(thread: Thread): void { + const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); for (const timelineSet of this.threadsTimelineSets) { timelineSet.removeEvent(thread.id); - timelineSet.addLiveEvent(thread.rootEvent); + timelineSet.addLiveEvent(thread.rootEvent, { + duplicateStrategy: DuplicateStrategy.Replace, + fromCache: false, + roomState, + }); } } @@ -1865,8 +1942,6 @@ export class Room extends ReadReceipt { this.lastThread = thread; } - this.emit(ThreadEvent.New, thread, toStartOfTimeline); - if (this.threadsReady) { this.threadsTimelineSets.forEach(timelineSet => { if (thread.rootEvent) { @@ -1883,6 +1958,8 @@ export class Room extends ReadReceipt { }); } + this.emit(ThreadEvent.New, thread, toStartOfTimeline); + return thread; } diff --git a/src/models/thread.ts b/src/models/thread.ts index 3c7add2f4..60757f9c3 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -51,11 +51,28 @@ interface IThreadOpts { client: MatrixClient; } +export enum FeatureSupport { + None = 0, + Experimental = 1, + Stable = 2 +} + +export function determineFeatureSupport(stable: boolean, unstable: boolean): FeatureSupport { + if (stable) { + return FeatureSupport.Stable; + } else if (unstable) { + return FeatureSupport.Experimental; + } else { + return FeatureSupport.None; + } +} + /** * @experimental */ export class Thread extends ReadReceipt { - public static hasServerSideSupport: boolean; + public static hasServerSideSupport = FeatureSupport.None; + public static hasServerSideListSupport = FeatureSupport.None; /** * A reference to all the events ID at the bottom of the threads @@ -134,15 +151,23 @@ export class Thread extends ReadReceipt { this.emit(ThreadEvent.Update, this); } - public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void { - Thread.hasServerSideSupport = hasServerSideSupport; - if (!useStable) { + public static setServerSideSupport( + status: FeatureSupport, + ): void { + Thread.hasServerSideSupport = status; + if (status !== FeatureSupport.Stable) { FILTER_RELATED_BY_SENDERS.setPreferUnstable(true); FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true); THREAD_RELATION_TYPE.setPreferUnstable(true); } } + public static setServerSideListSupport( + status: FeatureSupport, + ): void { + Thread.hasServerSideListSupport = status; + } + private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent) => { if (event?.isRelation(THREAD_RELATION_TYPE.name) && this.room.eventShouldLiveIn(event).threadId === this.id && From a1b046b5d88212c0776dea2fc3725fe2234a3168 Mon Sep 17 00:00:00 2001 From: Kerry Date: Thu, 6 Oct 2022 08:11:25 +0200 Subject: [PATCH 29/48] test typescriptification - spec/integ (#2714) * renamed: spec/integ/devicelist-integ.spec.js -> spec/integ/devicelist-integ.spec.ts * fix ts issue in devicelist-integ.spec * renamed: spec/integ/matrix-client-event-emitter.spec.js -> spec/integ/matrix-client-event-emitter.spec.ts * ts issues in matrix-client-event-emitter integ * strict fixes * renamed: spec/integ/matrix-client-methods.spec.js -> spec/integ/matrix-client-methods.spec.ts * fix ts issues * renamed: spec/integ/matrix-client-opts.spec.js -> spec/integ/matrix-client-opts.spec.ts * ts fixes in matrix-client-methods / matrix-client-opts * renamed: spec/integ/matrix-client-room-timeline.spec.js -> spec/integ/matrix-client-room-timeline.spec.ts * most ts fixes in matrix-client-room-timeline * remove obsoleted prev_events from mockenvents * make xmlhttprequest ts * strict errors in matrix-client-event-timeline spec * strict in devicelist * strict fixes in matrix-client-crypto.spec * strict fixes in spec/integ/matrix-client-room-timeline * strict issues in matrix-client-opts.specc * strict issues in matrix-client-syncing * strict issues in spec/integ/megolm * strict fixes in spec/integ/matrix-client-retrying.spec * strict fixes for spec/integ/sliding-sync * eslint fixes * more strict errors sneaking in from develop * kill al httpbackends * kill matrix-client-methods.spec httpbackend properly --- .../{setupTests.js => setupTests.ts} | 2 + ...integ.spec.js => devicelist-integ.spec.ts} | 19 +- spec/integ/matrix-client-crypto.spec.ts | 34 +- ...js => matrix-client-event-emitter.spec.ts} | 254 +++++------ .../matrix-client-event-timeline.spec.ts | 281 ++++++------ ....spec.js => matrix-client-methods.spec.ts} | 414 +++++++++++------- ...pts.spec.js => matrix-client-opts.spec.ts} | 16 +- spec/integ/matrix-client-retrying.spec.ts | 52 ++- ...js => matrix-client-room-timeline.spec.ts} | 320 +++++++------- spec/integ/matrix-client-syncing.spec.ts | 130 +++--- spec/integ/megolm-backup.spec.ts | 37 +- spec/integ/megolm-integ.spec.ts | 124 +++--- spec/integ/sliding-sync-sdk.spec.ts | 180 ++++---- spec/integ/sliding-sync.spec.ts | 156 +++---- 14 files changed, 1074 insertions(+), 945 deletions(-) rename spec/browserify/{setupTests.js => setupTests.ts} (95%) rename spec/integ/{devicelist-integ.spec.js => devicelist-integ.spec.ts} (95%) rename spec/integ/{matrix-client-event-emitter.spec.js => matrix-client-event-emitter.spec.ts} (51%) rename spec/integ/{matrix-client-methods.spec.js => matrix-client-methods.spec.ts} (77%) rename spec/integ/{matrix-client-opts.spec.js => matrix-client-opts.spec.ts} (93%) rename spec/integ/{matrix-client-room-timeline.spec.js => matrix-client-room-timeline.spec.ts} (76%) diff --git a/spec/browserify/setupTests.js b/spec/browserify/setupTests.ts similarity index 95% rename from spec/browserify/setupTests.js rename to spec/browserify/setupTests.ts index 16120f78a..833d8591c 100644 --- a/spec/browserify/setupTests.js +++ b/spec/browserify/setupTests.ts @@ -15,9 +15,11 @@ limitations under the License. */ // stub for browser-matrix browserify tests +// @ts-ignore global.XMLHttpRequest = jest.fn(); afterAll(() => { // clean up XMLHttpRequest mock + // @ts-ignore global.XMLHttpRequest = undefined; }); diff --git a/spec/integ/devicelist-integ.spec.js b/spec/integ/devicelist-integ.spec.ts similarity index 95% rename from spec/integ/devicelist-integ.spec.js rename to spec/integ/devicelist-integ.spec.ts index 8be2ca59a..acd8f9c80 100644 --- a/spec/integ/devicelist-integ.spec.js +++ b/spec/integ/devicelist-integ.spec.ts @@ -122,7 +122,7 @@ describe("DeviceList management:", function() { aliceTestClient.httpBackend.when( 'PUT', '/send/', ).respond(200, { - event_id: '$event_id', + event_id: '$event_id', }); return Promise.all([ @@ -290,8 +290,9 @@ describe("DeviceList management:", function() { aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; + // Alice should be tracking bob's device list expect(bobStat).toBeGreaterThan( - 0, "Alice should be tracking bob's device list", + 0, ); }); }); @@ -326,8 +327,9 @@ describe("DeviceList management:", function() { aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; + // Alice should have marked bob's device list as untracked expect(bobStat).toEqual( - 0, "Alice should have marked bob's device list as untracked", + 0, ); }); }); @@ -362,8 +364,9 @@ describe("DeviceList management:", function() { aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; + // Alice should have marked bob's device list as untracked expect(bobStat).toEqual( - 0, "Alice should have marked bob's device list as untracked", + 0, ); }); }); @@ -378,13 +381,15 @@ describe("DeviceList management:", function() { anotherTestClient.httpBackend.when('GET', '/sync').respond( 200, getSyncResponse([])); await anotherTestClient.flushSync(); - await anotherTestClient.client.crypto.deviceList.saveIfDirty(); + await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty(); + // @ts-ignore accessing private property anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; + const bobStat = data!.trackingStatus['@bob:xyz']; + // Alice should have marked bob's device list as untracked expect(bobStat).toEqual( - 0, "Alice should have marked bob's device list as untracked", + 0, ); }); } finally { diff --git a/spec/integ/matrix-client-crypto.spec.ts b/spec/integ/matrix-client-crypto.spec.ts index 262975ead..f86394979 100644 --- a/spec/integ/matrix-client-crypto.spec.ts +++ b/spec/integ/matrix-client-crypto.spec.ts @@ -31,8 +31,9 @@ import '../olm-loader'; import { logger } from '../../src/logger'; import * as testUtils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; -import { CRYPTO_ENABLED } from "../../src/client"; +import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client"; import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix"; +import { DeviceInfo } from '../../src/crypto/deviceinfo'; let aliTestClient: TestClient; const roomId = "!room:localhost"; @@ -71,12 +72,12 @@ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise { const keys = await bobTestClient.awaitOneTimeKeyUpload(); aliTestClient.httpBackend.when( "POST", "/keys/claim", - ).respond(200, function(_path, content) { - const claimType = content.one_time_keys[bobUserId][bobDeviceId]; + ).respond(200, function(_path, content: IUploadKeysRequest) { + const claimType = content.one_time_keys![bobUserId][bobDeviceId]; expect(claimType).toEqual("signed_curve25519"); - let keyId = null; + let keyId = ''; for (keyId in keys) { if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) { if (keyId.indexOf(claimType + ":") === 0) { @@ -135,10 +136,10 @@ async function aliDownloadsKeys(): Promise { await aliTestClient.client.crypto!.deviceList.saveIfDirty(); // @ts-ignore - protected aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const devices = data.devices[bobUserId]; + const devices = data!.devices[bobUserId]!; expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys); expect(devices[bobDeviceId].verified). - toBe(0); // DeviceVerification.UNVERIFIED + toBe(DeviceInfo.DeviceVerification.UNVERIFIED); }); } @@ -237,7 +238,7 @@ function sendMessage(client: MatrixClient): Promise { async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise { const path = "/send/m.room.encrypted/"; - const prom = new Promise((resolve) => { + const prom = new Promise((resolve) => { httpBackend.when("PUT", path).respond(200, function(_path, content) { resolve(content); return { @@ -252,14 +253,14 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): } function aliRecvMessage(): Promise { - const message = bobMessages.shift(); + const message = bobMessages.shift()!; return recvMessage( aliTestClient.httpBackend, aliTestClient.client, bobUserId, message, ); } function bobRecvMessage(): Promise { - const message = aliMessages.shift(); + const message = aliMessages.shift()!; return recvMessage( bobTestClient.httpBackend, bobTestClient.client, aliUserId, message, ); @@ -509,7 +510,7 @@ describe("MatrixClient crypto", () => { await firstSync(aliTestClient); await aliEnablesEncryption(); await aliSendsFirstMessage(); - const message = aliMessages.shift(); + const message = aliMessages.shift()!; const syncData = { next_batch: "x", rooms: { @@ -664,11 +665,10 @@ describe("MatrixClient crypto", () => { ]); logger.log(aliTestClient + ': started'); httpBackend.when("POST", "/keys/upload") - .respond(200, (_path, content) => { + .respond(200, (_path, content: IUploadKeysRequest) => { expect(content.one_time_keys).toBeTruthy(); expect(content.one_time_keys).not.toEqual({}); - expect(Object.keys(content.one_time_keys).length).toBeGreaterThanOrEqual(1); - logger.log('received %i one-time keys', Object.keys(content.one_time_keys).length); + expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1); // cancel futher calls by telling the client // we have more than we need return { diff --git a/spec/integ/matrix-client-event-emitter.spec.js b/spec/integ/matrix-client-event-emitter.spec.ts similarity index 51% rename from spec/integ/matrix-client-event-emitter.spec.js rename to spec/integ/matrix-client-event-emitter.spec.ts index bb3c873b3..1ad244b54 100644 --- a/spec/integ/matrix-client-event-emitter.spec.js +++ b/spec/integ/matrix-client-event-emitter.spec.ts @@ -1,25 +1,59 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import HttpBackend from "matrix-mock-request"; + +import { + ClientEvent, + HttpApiEvent, + IEvent, + MatrixClient, + RoomEvent, + RoomMemberEvent, + RoomStateEvent, + UserEvent, +} from "../../src"; import * as utils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; describe("MatrixClient events", function() { - let client; - let httpBackend; const selfUserId = "@alice:localhost"; const selfAccessToken = "aseukfgwef"; + let client: MatrixClient | undefined; + let httpBackend: HttpBackend | undefined; + + const setupTests = (): [MatrixClient, HttpBackend] => { + const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken); + const client = testClient.client; + const httpBackend = testClient.httpBackend; + httpBackend!.when("GET", "/versions").respond(200, {}); + httpBackend!.when("GET", "/pushrules").respond(200, {}); + httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" }); + + return [client!, httpBackend]; + }; beforeEach(function() { - const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken); - client = testClient.client; - httpBackend = testClient.httpBackend; - httpBackend.when("GET", "/versions").respond(200, {}); - httpBackend.when("GET", "/pushrules").respond(200, {}); - httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" }); + [client!, httpBackend] = setupTests(); }); afterEach(function() { - httpBackend.verifyNoOutstandingExpectation(); - client.stopClient(); - return httpBackend.stop(); + httpBackend?.verifyNoOutstandingExpectation(); + client?.stopClient(); + return httpBackend?.stop(); }); describe("emissions", function() { @@ -92,53 +126,49 @@ describe("MatrixClient events", function() { }; it("should emit events from both the first and subsequent /sync calls", - function() { - httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); - httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); + function() { + httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA); + httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); - let expectedEvents = []; - expectedEvents = expectedEvents.concat( - SYNC_DATA.presence.events, - SYNC_DATA.rooms.join["!erufh:bar"].timeline.events, - SYNC_DATA.rooms.join["!erufh:bar"].state.events, - NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events, - NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events, - ); + let expectedEvents: Partial[] = []; + expectedEvents = expectedEvents.concat( + SYNC_DATA.presence.events, + SYNC_DATA.rooms.join["!erufh:bar"].timeline.events, + SYNC_DATA.rooms.join["!erufh:bar"].state.events, + NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events, + NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events, + ); - client.on("event", function(event) { - let found = false; - for (let i = 0; i < expectedEvents.length; i++) { - if (expectedEvents[i].event_id === event.getId()) { - expectedEvents.splice(i, 1); - found = true; - break; + client!.on(ClientEvent.Event, function(event) { + let found = false; + for (let i = 0; i < expectedEvents.length; i++) { + if (expectedEvents[i].event_id === event.getId()) { + expectedEvents.splice(i, 1); + found = true; + break; + } } - } - expect(found).toBe( - true, "Unexpected 'event' emitted: " + event.getType(), - ); - }); + expect(found).toBe(true); + }); - client.startClient(); + client!.startClient(); - return Promise.all([ + return Promise.all([ // wait for two SYNCING events - utils.syncPromise(client).then(() => { - return utils.syncPromise(client); - }), - httpBackend.flushAllExpected(), - ]).then(() => { - expect(expectedEvents.length).toEqual( - 0, "Failed to see all events from /sync calls", - ); + utils.syncPromise(client!).then(() => { + return utils.syncPromise(client!); + }), + httpBackend!.flushAllExpected(), + ]).then(() => { + expect(expectedEvents.length).toEqual(0); + }); }); - }); it("should emit User events", function(done) { - httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); - httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); + httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA); + httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); let fired = false; - client.on("User.presence", function(event, user) { + client!.on(UserEvent.Presence, function(event, user) { fired = true; expect(user).toBeTruthy(); expect(event).toBeTruthy(); @@ -146,58 +176,52 @@ describe("MatrixClient events", function() { return; } - expect(event.event).toMatch(SYNC_DATA.presence.events[0]); + expect(event.event).toEqual(SYNC_DATA.presence.events[0]); expect(user.presence).toEqual( - SYNC_DATA.presence.events[0].content.presence, + SYNC_DATA.presence.events[0]?.content?.presence, ); }); - client.startClient(); + client!.startClient(); - httpBackend.flushAllExpected().then(function() { - expect(fired).toBe(true, "User.presence didn't fire."); + httpBackend!.flushAllExpected().then(function() { + expect(fired).toBe(true); done(); }); }); it("should emit Room events", function() { - httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); - httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); + httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA); + httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); let roomInvokeCount = 0; let roomNameInvokeCount = 0; let timelineFireCount = 0; - client.on("Room", function(room) { + client!.on(ClientEvent.Room, function(room) { roomInvokeCount++; expect(room.roomId).toEqual("!erufh:bar"); }); - client.on("Room.timeline", function(event, room) { + client!.on(RoomEvent.Timeline, function(event, room) { timelineFireCount++; expect(room.roomId).toEqual("!erufh:bar"); }); - client.on("Room.name", function(room) { + client!.on(RoomEvent.Name, function(room) { roomNameInvokeCount++; }); - client.startClient(); + client!.startClient(); return Promise.all([ - httpBackend.flushAllExpected(), - utils.syncPromise(client, 2), + httpBackend!.flushAllExpected(), + utils.syncPromise(client!, 2), ]).then(function() { - expect(roomInvokeCount).toEqual( - 1, "Room fired wrong number of times.", - ); - expect(roomNameInvokeCount).toEqual( - 1, "Room.name fired wrong number of times.", - ); - expect(timelineFireCount).toEqual( - 3, "Room.timeline fired the wrong number of times", - ); + expect(roomInvokeCount).toEqual(1); + expect(roomNameInvokeCount).toEqual(1); + expect(timelineFireCount).toEqual(3); }); }); it("should emit RoomState events", function() { - httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); - httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); + httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA); + httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); const roomStateEventTypes = [ "m.room.member", "m.room.create", @@ -205,126 +229,106 @@ describe("MatrixClient events", function() { let eventsInvokeCount = 0; let membersInvokeCount = 0; let newMemberInvokeCount = 0; - client.on("RoomState.events", function(event, state) { + client!.on(RoomStateEvent.Events, function(event, state) { eventsInvokeCount++; const index = roomStateEventTypes.indexOf(event.getType()); - expect(index).not.toEqual( - -1, "Unexpected room state event type: " + event.getType(), - ); + expect(index).not.toEqual(-1); if (index >= 0) { roomStateEventTypes.splice(index, 1); } }); - client.on("RoomState.members", function(event, state, member) { + client!.on(RoomStateEvent.Members, function(event, state, member) { membersInvokeCount++; expect(member.roomId).toEqual("!erufh:bar"); expect(member.userId).toEqual("@foo:bar"); expect(member.membership).toEqual("join"); }); - client.on("RoomState.newMember", function(event, state, member) { + client!.on(RoomStateEvent.NewMember, function(event, state, member) { newMemberInvokeCount++; expect(member.roomId).toEqual("!erufh:bar"); expect(member.userId).toEqual("@foo:bar"); expect(member.membership).toBeFalsy(); }); - client.startClient(); + client!.startClient(); return Promise.all([ - httpBackend.flushAllExpected(), - utils.syncPromise(client, 2), + httpBackend!.flushAllExpected(), + utils.syncPromise(client!, 2), ]).then(function() { - expect(membersInvokeCount).toEqual( - 1, "RoomState.members fired wrong number of times", - ); - expect(newMemberInvokeCount).toEqual( - 1, "RoomState.newMember fired wrong number of times", - ); - expect(eventsInvokeCount).toEqual( - 2, "RoomState.events fired wrong number of times", - ); + expect(membersInvokeCount).toEqual(1); + expect(newMemberInvokeCount).toEqual(1); + expect(eventsInvokeCount).toEqual(2); }); }); it("should emit RoomMember events", function() { - httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); - httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); + httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA); + httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); let typingInvokeCount = 0; let powerLevelInvokeCount = 0; let nameInvokeCount = 0; let membershipInvokeCount = 0; - client.on("RoomMember.name", function(event, member) { + client!.on(RoomMemberEvent.Name, function(event, member) { nameInvokeCount++; }); - client.on("RoomMember.typing", function(event, member) { + client!.on(RoomMemberEvent.Typing, function(event, member) { typingInvokeCount++; expect(member.typing).toBe(true); }); - client.on("RoomMember.powerLevel", function(event, member) { + client!.on(RoomMemberEvent.PowerLevel, function(event, member) { powerLevelInvokeCount++; }); - client.on("RoomMember.membership", function(event, member) { + client!.on(RoomMemberEvent.Membership, function(event, member) { membershipInvokeCount++; expect(member.membership).toEqual("join"); }); - client.startClient(); + client!.startClient(); return Promise.all([ - httpBackend.flushAllExpected(), - utils.syncPromise(client, 2), + httpBackend!.flushAllExpected(), + utils.syncPromise(client!, 2), ]).then(function() { - expect(typingInvokeCount).toEqual( - 1, "RoomMember.typing fired wrong number of times", - ); - expect(powerLevelInvokeCount).toEqual( - 0, "RoomMember.powerLevel fired wrong number of times", - ); - expect(nameInvokeCount).toEqual( - 0, "RoomMember.name fired wrong number of times", - ); - expect(membershipInvokeCount).toEqual( - 1, "RoomMember.membership fired wrong number of times", - ); + expect(typingInvokeCount).toEqual(1); + expect(powerLevelInvokeCount).toEqual(0); + expect(nameInvokeCount).toEqual(0); + expect(membershipInvokeCount).toEqual(1); }); }); it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() { const error = { errcode: 'M_UNKNOWN_TOKEN' }; - httpBackend.when("GET", "/sync").respond(401, error); + httpBackend!.when("GET", "/sync").respond(401, error); let sessionLoggedOutCount = 0; - client.on("Session.logged_out", function(errObj) { + client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) { sessionLoggedOutCount++; expect(errObj.data).toEqual(error); }); - client.startClient(); + client!.startClient(); - return httpBackend.flushAllExpected().then(function() { - expect(sessionLoggedOutCount).toEqual( - 1, "Session.logged_out fired wrong number of times", - ); + return httpBackend!.flushAllExpected().then(function() { + expect(sessionLoggedOutCount).toEqual(1); }); }); it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() { const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true }; - httpBackend.when("GET", "/sync").respond(401, error); + httpBackend!.when("GET", "/sync").respond(401, error); let sessionLoggedOutCount = 0; - client.on("Session.logged_out", function(errObj) { + client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) { sessionLoggedOutCount++; expect(errObj.data).toEqual(error); }); - client.startClient(); + client!.startClient(); - return httpBackend.flushAllExpected().then(function() { - expect(sessionLoggedOutCount).toEqual( - 1, "Session.logged_out fired wrong number of times", - ); + return httpBackend!.flushAllExpected().then(function() { + expect(sessionLoggedOutCount).toEqual(1); }); }); }); diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 8640e82be..f2bfa5f6a 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -142,6 +142,7 @@ const THREAD_REPLY = utils.mkEvent({ event: false, }); +// @ts-ignore we know this is a defined path for THREAD ROOT THREAD_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD_REPLY; const SYNC_THREAD_ROOT = withoutRoomId(THREAD_ROOT); @@ -214,8 +215,8 @@ describe("getEventTimeline support", function() { httpBackend = testClient.httpBackend; return startClient(httpBackend, client).then(function() { - const room = client.getRoom(roomId); - const timelineSet = room.getTimelineSets()[0]; + const room = client.getRoom(roomId)!; + const timelineSet = room!.getTimelineSets()[0]; expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy(); }); }); @@ -232,8 +233,8 @@ describe("getEventTimeline support", function() { httpBackend = testClient.httpBackend; return startClient(httpBackend, client).then(() => { - const room = client.getRoom(roomId); - const timelineSet = room.getTimelineSets()[0]; + const room = client.getRoom(roomId)!; + const timelineSet = room!.getTimelineSets()[0]; expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeFalsy(); }); }); @@ -257,10 +258,10 @@ describe("getEventTimeline support", function() { it("scrollback should be able to scroll back to before a gappy /sync", function() { // need a client with timelineSupport disabled to make this work - let room: Room; + let room: Room | undefined; return startClient(httpBackend, client).then(function() { - room = client.getRoom(roomId); + room = client.getRoom(roomId)!; httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", @@ -300,8 +301,8 @@ describe("getEventTimeline support", function() { utils.syncPromise(client, 2), ]); }).then(function() { - expect(room.timeline.length).toEqual(1); - expect(room.timeline[0].event).toEqual(EVENTS[1]); + expect(room!.timeline.length).toEqual(1); + expect(room!.timeline[0].event).toEqual(EVENTS[1]); httpBackend.when("GET", "/messages").respond(200, { chunk: [EVENTS[0]], @@ -309,12 +310,12 @@ describe("getEventTimeline support", function() { end: "pagin_end", }); httpBackend.flush("/messages", 1); - return client.scrollback(room); + return client.scrollback(room!); }).then(function() { - expect(room.timeline.length).toEqual(2); - expect(room.timeline[0].event).toEqual(EVENTS[0]); - expect(room.timeline[1].event).toEqual(EVENTS[1]); - expect(room.oldState.paginationToken).toEqual("pagin_end"); + expect(room!.timeline.length).toEqual(2); + expect(room!.timeline[0].event).toEqual(EVENTS[0]); + expect(room!.timeline[1].event).toEqual(EVENTS[1]); + expect(room!.oldState.paginationToken).toEqual("pagin_end"); }); }); }); @@ -345,7 +346,7 @@ describe("MatrixClient event timelines", function() { describe("getEventTimeline", function() { it("should create a new timeline for new events", function() { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar") .respond(200, function() { @@ -364,14 +365,14 @@ describe("MatrixClient event timelines", function() { return Promise.all([ client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) { - expect(tl.getEvents().length).toEqual(4); + expect(tl!.getEvents().length).toEqual(4); for (let i = 0; i < 4; i++) { - expect(tl.getEvents()[i].event).toEqual(EVENTS[i]); - expect(tl.getEvents()[i].sender.name).toEqual(userName); + expect(tl!.getEvents()[i].event).toEqual(EVENTS[i]); + expect(tl!.getEvents()[i]?.sender.name).toEqual(userName); } - expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) + expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token"); - expect(tl.getPaginationToken(EventTimeline.FORWARDS)) + expect(tl!.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token"); }), httpBackend.flushAllExpected(), @@ -379,7 +380,7 @@ describe("MatrixClient event timelines", function() { }); it("should return existing timeline for known events", function() { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", @@ -401,12 +402,12 @@ describe("MatrixClient event timelines", function() { httpBackend.flush("/sync"), utils.syncPromise(client), ]).then(function() { - return client.getEventTimeline(timelineSet, EVENTS[0].event_id); + return client.getEventTimeline(timelineSet, EVENTS[0].event_id!); }).then(function(tl) { - expect(tl.getEvents().length).toEqual(2); - expect(tl.getEvents()[1].event).toEqual(EVENTS[0]); - expect(tl.getEvents()[1].sender.name).toEqual(userName); - expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) + expect(tl!.getEvents().length).toEqual(2); + expect(tl!.getEvents()[1].event).toEqual(EVENTS[0]); + expect(tl!.getEvents()[1]?.sender.name).toEqual(userName); + expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("f_1_1"); // expect(tl.getPaginationToken(EventTimeline.FORWARDS)) // .toEqual("s_5_4"); @@ -414,7 +415,7 @@ describe("MatrixClient event timelines", function() { }); it("should update timelines where they overlap a previous /sync", function() { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", @@ -433,7 +434,7 @@ describe("MatrixClient event timelines", function() { }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + - encodeURIComponent(EVENTS[2].event_id)) + encodeURIComponent(EVENTS[2].event_id!)) .respond(200, function() { return { start: "start_token", @@ -447,13 +448,13 @@ describe("MatrixClient event timelines", function() { const prom = new Promise((resolve, reject) => { client.on(ClientEvent.Sync, function() { - client.getEventTimeline(timelineSet, EVENTS[2].event_id, + client.getEventTimeline(timelineSet, EVENTS[2].event_id!, ).then(function(tl) { - expect(tl.getEvents().length).toEqual(4); - expect(tl.getEvents()[0].event).toEqual(EVENTS[1]); - expect(tl.getEvents()[1].event).toEqual(EVENTS[2]); - expect(tl.getEvents()[3].event).toEqual(EVENTS[3]); - expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) + expect(tl!.getEvents().length).toEqual(4); + expect(tl!.getEvents()[0].event).toEqual(EVENTS[1]); + expect(tl!.getEvents()[1].event).toEqual(EVENTS[2]); + expect(tl!.getEvents()[3].event).toEqual(EVENTS[3]); + expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token"); // expect(tl.getPaginationToken(EventTimeline.FORWARDS)) // .toEqual("s_5_4"); @@ -468,13 +469,13 @@ describe("MatrixClient event timelines", function() { }); it("should join timelines where they overlap a previous /context", function() { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; // we fetch event 0, then 2, then 3, and finally 1. 1 is returned // with context which joins them all up. httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + - encodeURIComponent(EVENTS[0].event_id)) + encodeURIComponent(EVENTS[0].event_id!)) .respond(200, function() { return { start: "start_token0", @@ -487,7 +488,7 @@ describe("MatrixClient event timelines", function() { }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + - encodeURIComponent(EVENTS[2].event_id)) + encodeURIComponent(EVENTS[2].event_id!)) .respond(200, function() { return { start: "start_token2", @@ -500,7 +501,7 @@ describe("MatrixClient event timelines", function() { }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + - encodeURIComponent(EVENTS[3].event_id)) + encodeURIComponent(EVENTS[3].event_id!)) .respond(200, function() { return { start: "start_token3", @@ -513,7 +514,7 @@ describe("MatrixClient event timelines", function() { }); httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + - encodeURIComponent(EVENTS[1].event_id)) + encodeURIComponent(EVENTS[1].event_id!)) .respond(200, function() { return { start: "start_token4", @@ -528,26 +529,26 @@ describe("MatrixClient event timelines", function() { let tl0; let tl3; return Promise.all([ - client.getEventTimeline(timelineSet, EVENTS[0].event_id, + client.getEventTimeline(timelineSet, EVENTS[0].event_id!, ).then(function(tl) { - expect(tl.getEvents().length).toEqual(1); + expect(tl!.getEvents().length).toEqual(1); tl0 = tl; - return client.getEventTimeline(timelineSet, EVENTS[2].event_id); + return client.getEventTimeline(timelineSet, EVENTS[2].event_id!); }).then(function(tl) { - expect(tl.getEvents().length).toEqual(1); - return client.getEventTimeline(timelineSet, EVENTS[3].event_id); + expect(tl!.getEvents().length).toEqual(1); + return client.getEventTimeline(timelineSet, EVENTS[3].event_id!); }).then(function(tl) { - expect(tl.getEvents().length).toEqual(1); + expect(tl!.getEvents().length).toEqual(1); tl3 = tl; - return client.getEventTimeline(timelineSet, EVENTS[1].event_id); + return client.getEventTimeline(timelineSet, EVENTS[1].event_id!); }).then(function(tl) { // we expect it to get merged in with event 2 - expect(tl.getEvents().length).toEqual(2); - expect(tl.getEvents()[0].event).toEqual(EVENTS[1]); - expect(tl.getEvents()[1].event).toEqual(EVENTS[2]); - expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS)) + expect(tl!.getEvents().length).toEqual(2); + expect(tl!.getEvents()[0].event).toEqual(EVENTS[1]); + expect(tl!.getEvents()[1].event).toEqual(EVENTS[2]); + expect(tl!.getNeighbouringTimeline(EventTimeline.BACKWARDS)) .toBe(tl0); - expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS)) + expect(tl!.getNeighbouringTimeline(EventTimeline.FORWARDS)) .toBe(tl3); expect(tl0.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token0"); @@ -563,7 +564,7 @@ describe("MatrixClient event timelines", function() { }); it("should fail gracefully if there is no event field", function() { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; // we fetch event 0, then 2, then 3, and finally 1. 1 is returned // with context which joins them all up. @@ -595,11 +596,11 @@ describe("MatrixClient event timelines", function() { client.clientOpts.experimentalThreadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time - const room = client.getRoom(roomId); - const thread = room.createThread(THREAD_ROOT.event_id, undefined, [], false); + const room = client.getRoom(roomId)!; + const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false); const timelineSet = thread.timelineSet; - httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id)) + httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id!)) .respond(200, function() { return { start: "start_token0", @@ -611,13 +612,13 @@ describe("MatrixClient event timelines", function() { }; }); - httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id)) + httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!)) .respond(200, function() { return THREAD_ROOT; }); httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" + - encodeURIComponent(THREAD_ROOT.event_id) + "/" + + encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20") .respond(200, function() { return { @@ -627,13 +628,13 @@ describe("MatrixClient event timelines", function() { }; }); - const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id); + const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!); await httpBackend.flushAllExpected(); const timeline = await timelinePromise; - expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id)).toBeTruthy(); - expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id)).toBeTruthy(); + expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy(); + expect(timeline!.getEvents().find(e => e.getId() === THREAD_REPLY.event_id!)).toBeTruthy(); }); it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => { @@ -641,12 +642,12 @@ describe("MatrixClient event timelines", function() { client.clientOpts.experimentalThreadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const threadRoot = new MatrixEvent(THREAD_ROOT); - const thread = room.createThread(THREAD_ROOT.event_id, threadRoot, [threadRoot], false); - const timelineSet = room.getTimelineSets()[0]; + const thread = room.createThread(THREAD_ROOT.event_id!, threadRoot, [threadRoot], false)!; + const timelineSet = room.getTimelineSets()[0]!; - httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id)) + httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!)) .respond(200, function() { return { start: "start_token0", @@ -659,13 +660,13 @@ describe("MatrixClient event timelines", function() { }); const [timeline] = await Promise.all([ - client.getEventTimeline(timelineSet, THREAD_ROOT.event_id), + client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!), httpBackend.flushAllExpected(), ]); - expect(timeline).not.toBe(thread.liveTimeline); + expect(timeline!).not.toBe(thread.liveTimeline); expect(timelineSet.getTimelines()).toContain(timeline); - expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id)).toBeTruthy(); + expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy(); }); it("should return undefined when event is not in the thread that the given timelineSet is representing", () => { @@ -673,12 +674,12 @@ describe("MatrixClient event timelines", function() { client.clientOpts.experimentalThreadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const threadRoot = new MatrixEvent(THREAD_ROOT); - const thread = room.createThread(THREAD_ROOT.event_id, threadRoot, [threadRoot], false); + const thread = room.createThread(THREAD_ROOT.event_id!, threadRoot, [threadRoot], false); const timelineSet = thread.timelineSet; - httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id)) + httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id!)) .respond(200, function() { return { start: "start_token0", @@ -691,7 +692,7 @@ describe("MatrixClient event timelines", function() { }); return Promise.all([ - expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id)).resolves.toBeUndefined(), + expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeUndefined(), httpBackend.flushAllExpected(), ]); }); @@ -701,10 +702,10 @@ describe("MatrixClient event timelines", function() { client.clientOpts.experimentalThreadSupport = true; Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; - httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id)) + httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id!)) .respond(200, function() { return { start: "start_token0", @@ -717,7 +718,7 @@ describe("MatrixClient event timelines", function() { }); return Promise.all([ - expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id)).resolves.toBeUndefined(), + expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeUndefined(), httpBackend.flushAllExpected(), ]); }); @@ -726,10 +727,10 @@ describe("MatrixClient event timelines", function() { // @ts-ignore client.clientOpts.lazyLoadMembers = true; client.stopClient(); // we don't need the client to be syncing at this time - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; - const req = httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id)); + const req = httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id!)); req.respond(200, function() { return { start: "start_token0", @@ -741,11 +742,11 @@ describe("MatrixClient event timelines", function() { }; }); req.check((request) => { - expect(request.queryParams.filter).toEqual(JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)); + expect(request.queryParams?.filter).toEqual(JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)); }); await Promise.all([ - client.getEventTimeline(timelineSet, EVENTS[0].event_id), + client.getEventTimeline(timelineSet, EVENTS[0].event_id!), httpBackend.flushAllExpected(), ]); }); @@ -810,7 +811,7 @@ describe("MatrixClient event timelines", function() { }); it("should create a new timeline for new events", function() { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; const latestMessageId = 'event1:bar'; @@ -845,14 +846,14 @@ describe("MatrixClient event timelines", function() { // for `getEventTimeline` and make sure it's called with the // correct parameters. This doesn't feel too bad to make sure // `getLatestTimeline` is doing the right thing though. - expect(tl.getEvents().length).toEqual(4); + expect(tl!.getEvents().length).toEqual(4); for (let i = 0; i < 4; i++) { - expect(tl.getEvents()[i].event).toEqual(EVENTS[i]); - expect(tl.getEvents()[i].sender.name).toEqual(userName); + expect(tl!.getEvents()[i].event).toEqual(EVENTS[i]); + expect(tl!.getEvents()[i]?.sender.name).toEqual(userName); } - expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) + expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token"); - expect(tl.getPaginationToken(EventTimeline.FORWARDS)) + expect(tl!.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token"); }), httpBackend.flushAllExpected(), @@ -860,7 +861,7 @@ describe("MatrixClient event timelines", function() { }); it("should throw error when /messages does not return a message", () => { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/rooms/!foo%3Abar/messages") @@ -881,11 +882,11 @@ describe("MatrixClient event timelines", function() { describe("paginateEventTimeline", function() { it("should allow you to paginate backwards", function() { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + - encodeURIComponent(EVENTS[0].event_id)) + encodeURIComponent(EVENTS[0].event_id!)) .respond(200, function() { return { start: "start_token0", @@ -899,7 +900,7 @@ describe("MatrixClient event timelines", function() { httpBackend.when("GET", "/rooms/!foo%3Abar/messages") .check(function(req) { - const params = req.queryParams; + const params = req.queryParams!; expect(params.dir).toEqual("b"); expect(params.from).toEqual("start_token0"); expect(params.limit).toEqual("30"); @@ -912,19 +913,19 @@ describe("MatrixClient event timelines", function() { let tl; return Promise.all([ - client.getEventTimeline(timelineSet, EVENTS[0].event_id, + client.getEventTimeline(timelineSet, EVENTS[0].event_id!, ).then(function(tl0) { tl = tl0; return client.paginateEventTimeline(tl, { backwards: true }); }).then(function(success) { expect(success).toBeTruthy(); - expect(tl.getEvents().length).toEqual(3); - expect(tl.getEvents()[0].event).toEqual(EVENTS[2]); - expect(tl.getEvents()[1].event).toEqual(EVENTS[1]); - expect(tl.getEvents()[2].event).toEqual(EVENTS[0]); - expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) + expect(tl!.getEvents().length).toEqual(3); + expect(tl!.getEvents()[0].event).toEqual(EVENTS[2]); + expect(tl!.getEvents()[1].event).toEqual(EVENTS[1]); + expect(tl!.getEvents()[2].event).toEqual(EVENTS[0]); + expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token1"); - expect(tl.getPaginationToken(EventTimeline.FORWARDS)) + expect(tl!.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token0"); }), httpBackend.flushAllExpected(), @@ -932,11 +933,11 @@ describe("MatrixClient event timelines", function() { }); it("should stop paginating when it encounters no `end` token", () => { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + - encodeURIComponent(EVENTS[0].event_id)) + encodeURIComponent(EVENTS[0].event_id!)) .respond(200, () => ({ start: "start_token0", events_before: [], @@ -948,7 +949,7 @@ describe("MatrixClient event timelines", function() { httpBackend.when("GET", "/rooms/!foo%3Abar/messages") .check(function(req) { - const params = req.queryParams; + const params = req.queryParams!; expect(params.dir).toEqual("b"); expect(params.from).toEqual("start_token0"); expect(params.limit).toEqual("30"); @@ -959,23 +960,23 @@ describe("MatrixClient event timelines", function() { return Promise.all([ (async () => { - const tl = await client.getEventTimeline(timelineSet, EVENTS[0].event_id); - const success = await client.paginateEventTimeline(tl, { backwards: true }); + const tl = await client.getEventTimeline(timelineSet, EVENTS[0].event_id!); + const success = await client.paginateEventTimeline(tl!, { backwards: true }); expect(success).toBeFalsy(); - expect(tl.getEvents().length).toEqual(1); - expect(tl.getPaginationToken(EventTimeline.BACKWARDS)).toEqual(null); - expect(tl.getPaginationToken(EventTimeline.FORWARDS)).toEqual("end_token0"); + expect(tl!.getEvents().length).toEqual(1); + expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)).toEqual(null); + expect(tl!.getPaginationToken(EventTimeline.FORWARDS)).toEqual("end_token0"); })(), httpBackend.flushAllExpected(), ]); }); it("should allow you to paginate forwards", function() { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + - encodeURIComponent(EVENTS[0].event_id)) + encodeURIComponent(EVENTS[0].event_id!)) .respond(200, function() { return { start: "start_token0", @@ -989,7 +990,7 @@ describe("MatrixClient event timelines", function() { httpBackend.when("GET", "/rooms/!foo%3Abar/messages") .check(function(req) { - const params = req.queryParams; + const params = req.queryParams!; expect(params.dir).toEqual("f"); expect(params.from).toEqual("end_token0"); expect(params.limit).toEqual("20"); @@ -1002,20 +1003,20 @@ describe("MatrixClient event timelines", function() { let tl; return Promise.all([ - client.getEventTimeline(timelineSet, EVENTS[0].event_id, + client.getEventTimeline(timelineSet, EVENTS[0].event_id!, ).then(function(tl0) { tl = tl0; return client.paginateEventTimeline( tl, { backwards: false, limit: 20 }); }).then(function(success) { expect(success).toBeTruthy(); - expect(tl.getEvents().length).toEqual(3); - expect(tl.getEvents()[0].event).toEqual(EVENTS[0]); - expect(tl.getEvents()[1].event).toEqual(EVENTS[1]); - expect(tl.getEvents()[2].event).toEqual(EVENTS[2]); - expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) + expect(tl!.getEvents().length).toEqual(3); + expect(tl!.getEvents()[0].event).toEqual(EVENTS[0]); + expect(tl!.getEvents()[1].event).toEqual(EVENTS[1]); + expect(tl!.getEvents()[2].event).toEqual(EVENTS[2]); + expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token0"); - expect(tl.getPaginationToken(EventTimeline.FORWARDS)) + expect(tl!.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token1"); }), httpBackend.flushAllExpected(), @@ -1285,17 +1286,17 @@ describe("MatrixClient event timelines", function() { }); it("should work when /send returns before /sync", function() { - const room = client.getRoom(roomId); - const timelineSet = room.getTimelineSets()[0]; + const room = client.getRoom(roomId)!; + const timelineSet = room.getTimelineSets()[0]!; return Promise.all([ client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) { - expect(res.event_id).toEqual(event.event_id); - return client.getEventTimeline(timelineSet, event.event_id); + expect(res.event_id).toEqual(event.event_id!); + return client.getEventTimeline(timelineSet, event.event_id!); }).then(function(tl) { // 2 because the initial sync contained an event - expect(tl.getEvents().length).toEqual(2); - expect(tl.getEvents()[1].getContent().body).toEqual("a body"); + expect(tl!.getEvents().length).toEqual(2); + expect(tl!.getEvents()[1].getContent().body).toEqual("a body"); // now let the sync complete, and check it again return Promise.all([ @@ -1303,10 +1304,10 @@ describe("MatrixClient event timelines", function() { utils.syncPromise(client), ]); }).then(function() { - return client.getEventTimeline(timelineSet, event.event_id); + return client.getEventTimeline(timelineSet, event.event_id!); }).then(function(tl) { - expect(tl.getEvents().length).toEqual(2); - expect(tl.getEvents()[1].event).toEqual(event); + expect(tl!.getEvents().length).toEqual(2); + expect(tl!.getEvents()[1].event).toEqual(event); }), httpBackend.flush("/send/m.room.message/" + TXN_ID, 1), @@ -1314,7 +1315,7 @@ describe("MatrixClient event timelines", function() { }); it("should work when /send returns after /sync", function() { - const room = client.getRoom(roomId); + const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; return Promise.all([ @@ -1322,23 +1323,23 @@ describe("MatrixClient event timelines", function() { // - but note that it won't complete until after the /sync does, below. client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) { logger.log("sendTextMessage completed"); - expect(res.event_id).toEqual(event.event_id); - return client.getEventTimeline(timelineSet, event.event_id); + expect(res.event_id).toEqual(event.event_id!); + return client.getEventTimeline(timelineSet, event.event_id!); }).then(function(tl) { logger.log("getEventTimeline completed (2)"); - expect(tl.getEvents().length).toEqual(2); - expect(tl.getEvents()[1].getContent().body).toEqual("a body"); + expect(tl!.getEvents().length).toEqual(2); + expect(tl!.getEvents()[1].getContent().body).toEqual("a body"); }), Promise.all([ httpBackend.flush("/sync", 1), utils.syncPromise(client), ]).then(function() { - return client.getEventTimeline(timelineSet, event.event_id); + return client.getEventTimeline(timelineSet, event.event_id!); }).then(function(tl) { logger.log("getEventTimeline completed (1)"); - expect(tl.getEvents().length).toEqual(2); - expect(tl.getEvents()[1].event).toEqual(event); + expect(tl!.getEvents().length).toEqual(2); + expect(tl!.getEvents()[1].event).toEqual(event); // now let the send complete. return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1); @@ -1382,10 +1383,10 @@ describe("MatrixClient event timelines", function() { httpBackend.flushAllExpected(), utils.syncPromise(client), ]).then(function() { - const room = client.getRoom(roomId); - const tl = room.getLiveTimeline(); - expect(tl.getEvents().length).toEqual(3); - expect(tl.getEvents()[1].isRedacted()).toBe(true); + const room = client.getRoom(roomId)!; + const tl = room.getLiveTimeline()!; + expect(tl!.getEvents().length).toEqual(3); + expect(tl!.getEvents()[1].isRedacted()).toBe(true); const sync2 = { next_batch: "batch2", @@ -1411,8 +1412,8 @@ describe("MatrixClient event timelines", function() { utils.syncPromise(client), ]); }).then(function() { - const room = client.getRoom(roomId); - const tl = room.getLiveTimeline(); + const room = client.getRoom(roomId)!; + const tl = room.getLiveTimeline()!; expect(tl.getEvents().length).toEqual(1); }); }); @@ -1439,11 +1440,11 @@ describe("MatrixClient event timelines", function() { }); await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]); - const room = client.getRoom(roomId); - const thread = room.getThread(THREAD_ROOT.event_id); + const room = client.getRoom(roomId)!; + const thread = room.getThread(THREAD_ROOT.event_id!)!; const timelineSet = thread.timelineSet; - httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id)) + httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!)) .respond(200, { start: "start_token", events_before: [], @@ -1453,7 +1454,7 @@ describe("MatrixClient event timelines", function() { end: "end_token", }); httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" + - encodeURIComponent(THREAD_ROOT.event_id) + "/" + + encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20") .respond(200, function() { return { @@ -1463,7 +1464,7 @@ describe("MatrixClient event timelines", function() { }; }); await Promise.all([ - client.getEventTimeline(timelineSet, THREAD_ROOT.event_id), + client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!), httpBackend.flushAllExpected(), ]); diff --git a/spec/integ/matrix-client-methods.spec.js b/spec/integ/matrix-client-methods.spec.ts similarity index 77% rename from spec/integ/matrix-client-methods.spec.js rename to spec/integ/matrix-client-methods.spec.ts index 69bfa89ca..fdf32d9e9 100644 --- a/spec/integ/matrix-client-methods.spec.js +++ b/spec/integ/matrix-client-methods.spec.ts @@ -13,68 +13,84 @@ 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 HttpBackend from "matrix-mock-request"; import * as utils from "../test-utils/test-utils"; -import { CRYPTO_ENABLED } from "../../src/client"; +import { CRYPTO_ENABLED, MatrixClient, IStoredClientOpts } from "../../src/client"; import { MatrixEvent } from "../../src/models/event"; import { Filter, MemoryStore, Room } from "../../src/matrix"; import { TestClient } from "../TestClient"; import { THREAD_RELATION_TYPE } from "../../src/models/thread"; +import { IFilterDefinition } from "../../src/filter"; +import { FileType } from "../../src/http-api"; +import { ISearchResults } from "../../src/@types/search"; +import { IStore } from "../../src/store"; describe("MatrixClient", function() { - let client = null; - let httpBackend = null; - let store = null; const userId = "@alice:localhost"; const accessToken = "aseukfgwef"; const idServerDomain = "identity.localhost"; // not a real server const identityAccessToken = "woop-i-am-a-secret"; + let client: MatrixClient | undefined; + let httpBackend: HttpBackend | undefined; + let store: MemoryStore | undefined; - beforeEach(function() { - store = new MemoryStore(); + const defaultClientOpts: IStoredClientOpts = { + canResetEntireTimeline: roomId => false, + experimentalThreadSupport: false, + crypto: {} as unknown as IStoredClientOpts['crypto'], + }; + const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => { + const store = new MemoryStore(); const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, { - store, + store: store as IStore, identityServer: { getAccessToken: () => Promise.resolve(identityAccessToken), }, idBaseUrl: `https://${idServerDomain}`, }); - httpBackend = testClient.httpBackend; - client = testClient.client; + + return [testClient.client, testClient.httpBackend, store]; + }; + + beforeEach(function() { + [client, httpBackend, store] = setupTests(); }); afterEach(function() { - httpBackend.verifyNoOutstandingExpectation(); - return httpBackend.stop(); + httpBackend!.verifyNoOutstandingExpectation(); + return httpBackend!.stop(); }); describe("uploadContent", function() { const buf = Buffer.from('hello world'); it("should upload the file", function() { - httpBackend.when( + httpBackend!.when( "POST", "/_matrix/media/r0/upload", ).check(function(req) { expect(req.rawData).toEqual(buf); - expect(req.queryParams.filename).toEqual("hi.txt"); - if (!(req.queryParams.access_token == accessToken || + expect(req.queryParams?.filename).toEqual("hi.txt"); + if (!(req.queryParams?.access_token == accessToken || req.headers["Authorization"] == "Bearer " + accessToken)) { expect(true).toBe(false); } expect(req.headers["Content-Type"]).toEqual("text/plain"); + // @ts-ignore private property expect(req.opts.json).toBeFalsy(); + // @ts-ignore private property expect(req.opts.timeout).toBe(undefined); }).respond(200, "content", true); - const prom = client.uploadContent({ + const prom = client!.uploadContent({ stream: buf, name: "hi.txt", type: "text/plain", - }); + } as unknown as FileType); expect(prom).toBeTruthy(); - const uploads = client.getCurrentUploads(); + const uploads = client!.getCurrentUploads(); expect(uploads.length).toEqual(1); expect(uploads[0].promise).toBe(prom); expect(uploads[0].loaded).toEqual(0); @@ -83,51 +99,53 @@ describe("MatrixClient", function() { // for backwards compatibility, we return the raw JSON expect(response).toEqual("content"); - const uploads = client.getCurrentUploads(); + const uploads = client!.getCurrentUploads(); expect(uploads.length).toEqual(0); }); - httpBackend.flush(); + httpBackend!.flush(''); return prom2; }); it("should parse the response if rawResponse=false", function() { - httpBackend.when( + httpBackend!.when( "POST", "/_matrix/media/r0/upload", ).check(function(req) { + // @ts-ignore private property expect(req.opts.json).toBeFalsy(); }).respond(200, { "content_uri": "uri" }); - const prom = client.uploadContent({ + const prom = client!.uploadContent({ stream: buf, name: "hi.txt", type: "text/plain", - }, { + } as unknown as FileType, { rawResponse: false, }).then(function(response) { expect(response.content_uri).toEqual("uri"); }); - httpBackend.flush(); + httpBackend!.flush(''); return prom; }); it("should parse errors into a MatrixError", function() { - httpBackend.when( + httpBackend!.when( "POST", "/_matrix/media/r0/upload", ).check(function(req) { expect(req.rawData).toEqual(buf); + // @ts-ignore private property expect(req.opts.json).toBeFalsy(); }).respond(400, { "errcode": "M_SNAFU", "error": "broken", }); - const prom = client.uploadContent({ + const prom = client!.uploadContent({ stream: buf, name: "hi.txt", type: "text/plain", - }).then(function(response) { + } as unknown as FileType).then(function(response) { throw Error("request not failed"); }, function(error) { expect(error.httpStatus).toEqual(400); @@ -135,18 +153,18 @@ describe("MatrixClient", function() { expect(error.message).toEqual("broken"); }); - httpBackend.flush(); + httpBackend!.flush(''); return prom; }); it("should return a promise which can be cancelled", function() { - const prom = client.uploadContent({ + const prom = client!.uploadContent({ stream: buf, name: "hi.txt", type: "text/plain", - }); + } as unknown as FileType); - const uploads = client.getCurrentUploads(); + const uploads = client!.getCurrentUploads(); expect(uploads.length).toEqual(1); expect(uploads[0].promise).toBe(prom); expect(uploads[0].loaded).toEqual(0); @@ -156,11 +174,11 @@ describe("MatrixClient", function() { }, function(error) { expect(error).toEqual("aborted"); - const uploads = client.getCurrentUploads(); + const uploads = client!.getCurrentUploads(); expect(uploads.length).toEqual(0); }); - const r = client.cancelUpload(prom); + const r = client!.cancelUpload(prom); expect(r).toBe(true); return prom2; }); @@ -169,17 +187,20 @@ describe("MatrixClient", function() { describe("joinRoom", function() { it("should no-op if you've already joined a room", function() { const roomId = "!foo:bar"; - const room = new Room(roomId, client, userId); - client.fetchRoomEvent = () => Promise.resolve({}); + const room = new Room(roomId, client!, userId); + client!.fetchRoomEvent = () => Promise.resolve({ + type: 'test', + content: {}, + }); room.addLiveEvents([ utils.mkMembership({ user: userId, room: roomId, mship: "join", event: true, }), ]); - httpBackend.verifyNoOutstandingRequests(); - store.storeRoom(room); - client.joinRoom(roomId); - httpBackend.verifyNoOutstandingRequests(); + httpBackend!.verifyNoOutstandingRequests(); + store!.storeRoom(room); + client!.joinRoom(roomId); + httpBackend!.verifyNoOutstandingRequests(); }); }); @@ -190,67 +211,67 @@ describe("MatrixClient", function() { const filter = Filter.fromJson(userId, filterId, { event_format: "client", }); - store.storeFilter(filter); - client.getFilter(userId, filterId, true).then(function(gotFilter) { + store!.storeFilter(filter); + client!.getFilter(userId, filterId, true).then(function(gotFilter) { expect(gotFilter).toEqual(filter); done(); }); - httpBackend.verifyNoOutstandingRequests(); + httpBackend!.verifyNoOutstandingRequests(); }); it("should do an HTTP request if !allowCached even if one exists", - function(done) { - const httpFilterDefinition = { - event_format: "federation", - }; + function(done) { + const httpFilterDefinition = { + event_format: "federation", + }; - httpBackend.when( - "GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId, - ).respond(200, httpFilterDefinition); + httpBackend!.when( + "GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId, + ).respond(200, httpFilterDefinition); - const storeFilter = Filter.fromJson(userId, filterId, { - event_format: "client", + const storeFilter = Filter.fromJson(userId, filterId, { + event_format: "client", + }); + store!.storeFilter(storeFilter); + client!.getFilter(userId, filterId, false).then(function(gotFilter) { + expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition); + done(); + }); + + httpBackend!.flush(''); }); - store.storeFilter(storeFilter); - client.getFilter(userId, filterId, false).then(function(gotFilter) { - expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition); - done(); - }); - - httpBackend.flush(); - }); it("should do an HTTP request if nothing is in the cache and then store it", - function(done) { - const httpFilterDefinition = { - event_format: "federation", - }; - expect(store.getFilter(userId, filterId)).toBe(null); + function(done) { + const httpFilterDefinition = { + event_format: "federation", + }; + expect(store!.getFilter(userId, filterId)).toBe(null); - httpBackend.when( - "GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId, - ).respond(200, httpFilterDefinition); - client.getFilter(userId, filterId, true).then(function(gotFilter) { - expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition); - expect(store.getFilter(userId, filterId)).toBeTruthy(); - done(); + httpBackend!.when( + "GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId, + ).respond(200, httpFilterDefinition); + client!.getFilter(userId, filterId, true).then(function(gotFilter) { + expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition); + expect(store!.getFilter(userId, filterId)).toBeTruthy(); + done(); + }); + + httpBackend!.flush(''); }); - - httpBackend.flush(); - }); }); describe("createFilter", function() { const filterId = "f1llllllerid"; it("should do an HTTP request and then store the filter", function(done) { - expect(store.getFilter(userId, filterId)).toBe(null); + expect(store!.getFilter(userId, filterId)).toBe(null); const filterDefinition = { - event_format: "client", + event_format: "client" as IFilterDefinition['event_format'], }; - httpBackend.when( + httpBackend!.when( "POST", "/user/" + encodeURIComponent(userId) + "/filter", ).check(function(req) { expect(req.data).toEqual(filterDefinition); @@ -258,13 +279,13 @@ describe("MatrixClient", function() { filter_id: filterId, }); - client.createFilter(filterDefinition).then(function(gotFilter) { + client!.createFilter(filterDefinition).then(function(gotFilter) { expect(gotFilter.getDefinition()).toEqual(filterDefinition); - expect(store.getFilter(userId, filterId)).toEqual(gotFilter); + expect(store!.getFilter(userId, filterId)).toEqual(gotFilter); done(); }); - httpBackend.flush(); + httpBackend!.flush(''); }); }); @@ -291,10 +312,10 @@ describe("MatrixClient", function() { }, }; - client.searchMessageText({ + client!.searchMessageText({ query: "monkeys", }); - httpBackend.when("POST", "/search").check(function(req) { + httpBackend!.when("POST", "/search").check(function(req) { expect(req.data).toEqual({ search_categories: { room_events: { @@ -304,7 +325,7 @@ describe("MatrixClient", function() { }); }).respond(200, response); - return httpBackend.flush(); + return httpBackend!.flush(''); }); describe("should filter out context from different timelines (threads)", () => { @@ -313,11 +334,14 @@ describe("MatrixClient", function() { search_categories: { room_events: { count: 24, + highlights: [], results: [{ rank: 0.1, result: { event_id: "$flibble:localhost", type: "m.room.message", + sender: '@test:locahost', + origin_server_ts: 123, user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { @@ -326,9 +350,12 @@ describe("MatrixClient", function() { }, }, context: { + profile_info: {}, events_after: [{ event_id: "$ev-after:server", type: "m.room.message", + sender: '@test:locahost', + origin_server_ts: 123, user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { @@ -343,6 +370,8 @@ describe("MatrixClient", function() { events_before: [{ event_id: "$ev-before:server", type: "m.room.message", + sender: '@test:locahost', + origin_server_ts: 123, user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { @@ -356,15 +385,17 @@ describe("MatrixClient", function() { }, }; - const data = { + const data: ISearchResults = { results: [], highlights: [], }; - client.processRoomEventsSearch(data, response); + client!.processRoomEventsSearch(data, response); expect(data.results).toHaveLength(1); - expect(data.results[0].context.timeline).toHaveLength(2); - expect(data.results[0].context.timeline.find(e => e.getId() === "$ev-after:server")).toBeFalsy(); + expect(data.results[0].context.getTimeline()).toHaveLength(2); + expect( + data.results[0].context.getTimeline().find(e => e.getId() === "$ev-after:server"), + ).toBeFalsy(); }); it("filters out thread replies from threads other than the thread the result replied to", () => { @@ -372,11 +403,14 @@ describe("MatrixClient", function() { search_categories: { room_events: { count: 24, + highlights: [], results: [{ rank: 0.1, result: { event_id: "$flibble:localhost", type: "m.room.message", + sender: '@test:locahost', + origin_server_ts: 123, user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { @@ -389,9 +423,12 @@ describe("MatrixClient", function() { }, }, context: { + profile_info: {}, events_after: [{ event_id: "$ev-after:server", type: "m.room.message", + sender: '@test:locahost', + origin_server_ts: 123, user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", content: { @@ -410,15 +447,17 @@ describe("MatrixClient", function() { }, }; - const data = { + const data: ISearchResults = { results: [], highlights: [], }; - client.processRoomEventsSearch(data, response); + client!.processRoomEventsSearch(data, response); expect(data.results).toHaveLength(1); - expect(data.results[0].context.timeline).toHaveLength(1); - expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy(); + expect(data.results[0].context.getTimeline()).toHaveLength(1); + expect( + data.results[0].context.getTimeline().find(e => e.getId() === "$flibble:localhost"), + ).toBeTruthy(); }); it("filters out main timeline events when result is a thread reply", () => { @@ -426,10 +465,13 @@ describe("MatrixClient", function() { search_categories: { room_events: { count: 24, + highlights: [], results: [{ rank: 0.1, result: { event_id: "$flibble:localhost", + sender: '@test:locahost', + origin_server_ts: 123, type: "m.room.message", user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", @@ -445,6 +487,8 @@ describe("MatrixClient", function() { context: { events_after: [{ event_id: "$ev-after:server", + sender: '@test:locahost', + origin_server_ts: 123, type: "m.room.message", user_id: "@alice:localhost", room_id: "!feuiwhf:localhost", @@ -454,21 +498,24 @@ describe("MatrixClient", function() { }, }], events_before: [], + profile_info: {}, }, }], }, }, }; - const data = { + const data: ISearchResults = { results: [], highlights: [], }; - client.processRoomEventsSearch(data, response); + client!.processRoomEventsSearch(data, response); expect(data.results).toHaveLength(1); - expect(data.results[0].context.timeline).toHaveLength(1); - expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy(); + expect(data.results[0].context.getTimeline()).toHaveLength(1); + expect( + data.results[0].context.getTimeline().find(e => e.getId() === "$flibble:localhost"), + ).toBeTruthy(); }); }); }); @@ -479,16 +526,16 @@ describe("MatrixClient", function() { } beforeEach(function() { - return client.initCrypto(); + return client!.initCrypto(); }); afterEach(() => { - client.stopClient(); + client!.stopClient(); }); it("should do an HTTP request and then store the keys", function() { const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78"; - // ed25519key = client.getDeviceEd25519Key(); + // ed25519key = client!.getDeviceEd25519Key(); const borisKeys = { dev1: { algorithms: ["1"], @@ -512,7 +559,7 @@ describe("MatrixClient", function() { keys: { "ed25519:dev2": ed25519key }, signatures: { chaz: { - "ed25519:dev2": + "ed25519:dev2": "FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" + "EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ", }, @@ -528,7 +575,7 @@ describe("MatrixClient", function() { var b = JSON.parse(JSON.stringify(o)); delete(b.signatures); delete(b.unsigned); - return client.crypto.olmDevice.sign(anotherjson.stringify(b)); + return client!.crypto.olmDevice.sign(anotherjson.stringify(b)); }; logger.log("Ed25519: " + ed25519key); @@ -536,7 +583,7 @@ describe("MatrixClient", function() { logger.log("chaz:", sign(chazKeys.dev2)); */ - httpBackend.when("POST", "/keys/query").check(function(req) { + httpBackend!.when("POST", "/keys/query").check(function(req) { expect(req.data).toEqual({ device_keys: { 'boris': [], 'chaz': [], @@ -548,7 +595,7 @@ describe("MatrixClient", function() { }, }); - const prom = client.downloadKeys(["boris", "chaz"]).then(function(res) { + const prom = client!.downloadKeys(["boris", "chaz"]).then(function(res) { assertObjectContains(res.boris.dev1, { verified: 0, // DeviceVerification.UNVERIFIED keys: { "ed25519:dev1": ed25519key }, @@ -564,23 +611,23 @@ describe("MatrixClient", function() { }); }); - httpBackend.flush(); + httpBackend!.flush(''); return prom; }); }); describe("deleteDevice", function() { - const auth = { a: 1 }; + const auth = { identifier: 1 }; it("should pass through an auth dict", function() { - httpBackend.when( + httpBackend!.when( "DELETE", "/_matrix/client/r0/devices/my_device", ).check(function(req) { expect(req.data).toEqual({ auth: auth }); }).respond(200); - const prom = client.deleteDevice("my_device", auth); + const prom = client!.deleteDevice("my_device", auth); - httpBackend.flush(); + httpBackend!.flush(''); return prom; }); }); @@ -588,7 +635,7 @@ describe("MatrixClient", function() { describe("partitionThreadedEvents", function() { let room; beforeEach(() => { - room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client, userId); + room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client!, userId); }); it("returns empty arrays when given an empty arrays", function() { @@ -599,7 +646,11 @@ describe("MatrixClient", function() { }); it("copies pre-thread in-timeline vote events onto both timelines", function() { - client.clientOpts = { experimentalThreadSupport: true }; + // @ts-ignore setting private property + client!.clientOpts = { + ...defaultClientOpts, + experimentalThreadSupport: true, + }; const eventPollResponseReference = buildEventPollResponseReference(); const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); @@ -611,6 +662,7 @@ describe("MatrixClient", function() { eventPollResponseReference, ]; // Vote has no threadId yet + // @ts-ignore private property expect(eventPollResponseReference.threadId).toBeFalsy(); const [timeline, threaded] = room.partitionThreadedEvents(events); @@ -634,7 +686,11 @@ describe("MatrixClient", function() { }); it("copies pre-thread in-timeline reactions onto both timelines", function() { - client.clientOpts = { experimentalThreadSupport: true }; + // @ts-ignore setting private property + client!.clientOpts = { + ...defaultClientOpts, + experimentalThreadSupport: true, + }; const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot); @@ -661,7 +717,11 @@ describe("MatrixClient", function() { }); it("copies post-thread in-timeline vote events onto both timelines", function() { - client.clientOpts = { experimentalThreadSupport: true }; + // @ts-ignore setting private property + client!.clientOpts = { + ...defaultClientOpts, + experimentalThreadSupport: true, + }; const eventPollResponseReference = buildEventPollResponseReference(); const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); @@ -688,7 +748,11 @@ describe("MatrixClient", function() { }); it("copies post-thread in-timeline reactions onto both timelines", function() { - client.clientOpts = { experimentalThreadSupport: true }; + // @ts-ignore setting private property + client!.clientOpts = { + ...defaultClientOpts, + experimentalThreadSupport: true, + }; const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot); @@ -715,7 +779,11 @@ describe("MatrixClient", function() { }); it("sends room state events to the main timeline only", function() { - client.clientOpts = { experimentalThreadSupport: true }; + // @ts-ignore setting private property + client!.clientOpts = { + ...defaultClientOpts, + experimentalThreadSupport: true, + }; // This is based on recording the events in a real room: const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); @@ -768,7 +836,11 @@ describe("MatrixClient", function() { }); it("sends redactions of reactions to thread responses to thread timeline only", () => { - client.clientOpts = { experimentalThreadSupport: true }; + // @ts-ignore setting private property + client!.clientOpts = { + ...defaultClientOpts, + experimentalThreadSupport: true, + }; const threadRootEvent = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(threadRootEvent); @@ -797,7 +869,11 @@ describe("MatrixClient", function() { }); it("sends reply to reply to thread root outside of thread to main timeline only", () => { - client.clientOpts = { experimentalThreadSupport: true }; + // @ts-ignore setting private property + client!.clientOpts = { + ...defaultClientOpts, + experimentalThreadSupport: true, + }; const threadRootEvent = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(threadRootEvent); @@ -826,7 +902,11 @@ describe("MatrixClient", function() { }); it("sends reply to thread responses to main timeline only", () => { - client.clientOpts = { experimentalThreadSupport: true }; + // @ts-ignore setting private property + client!.clientOpts = { + ...defaultClientOpts, + experimentalThreadSupport: true, + }; const threadRootEvent = buildEventPollStartThreadRoot(); const eventMessageInThread = buildEventMessageInThread(threadRootEvent); @@ -860,9 +940,9 @@ describe("MatrixClient", function() { fields: {}, }]; - const prom = client.getThirdpartyUser("irc", {}); - httpBackend.when("GET", "/thirdparty/user/irc").respond(200, response); - await httpBackend.flush(); + const prom = client!.getThirdpartyUser("irc", {}); + httpBackend!.when("GET", "/thirdparty/user/irc").respond(200, response); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -875,9 +955,9 @@ describe("MatrixClient", function() { fields: {}, }]; - const prom = client.getThirdpartyLocation("irc", {}); - httpBackend.when("GET", "/thirdparty/location/irc").respond(200, response); - await httpBackend.flush(); + const prom = client!.getThirdpartyLocation("irc", {}); + httpBackend!.when("GET", "/thirdparty/location/irc").respond(200, response); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -888,10 +968,10 @@ describe("MatrixClient", function() { pushers: [], }; - const prom = client.getPushers(); - httpBackend.when("GET", "/_matrix/client/versions").respond(200, {}); - httpBackend.when("GET", "/pushers").respond(200, response); - await httpBackend.flush(); + const prom = client!.getPushers(); + httpBackend!.when("GET", "/_matrix/client/versions").respond(200, {}); + httpBackend!.when("GET", "/pushers").respond(200, response); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -903,12 +983,12 @@ describe("MatrixClient", function() { left: [], }; - const prom = client.getKeyChanges("old", "new"); - httpBackend.when("GET", "/keys/changes").check((req) => { - expect(req.queryParams.from).toEqual("old"); - expect(req.queryParams.to).toEqual("new"); + const prom = client!.getKeyChanges("old", "new"); + httpBackend!.when("GET", "/keys/changes").check((req) => { + expect(req.queryParams?.from).toEqual("old"); + expect(req.queryParams?.to).toEqual("new"); }).respond(200, response); - await httpBackend.flush(); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -919,9 +999,9 @@ describe("MatrixClient", function() { devices: [], }; - const prom = client.getDevices(); - httpBackend.when("GET", "/devices").respond(200, response); - await httpBackend.flush(); + const prom = client!.getDevices(); + httpBackend!.when("GET", "/devices").respond(200, response); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -935,9 +1015,9 @@ describe("MatrixClient", function() { last_seen_ts: 1, }; - const prom = client.getDevice("DEADBEEF"); - httpBackend.when("GET", "/devices/DEADBEEF").respond(200, response); - await httpBackend.flush(); + const prom = client!.getDevice("DEADBEEF"); + httpBackend!.when("GET", "/devices/DEADBEEF").respond(200, response); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -948,9 +1028,9 @@ describe("MatrixClient", function() { threepids: [], }; - const prom = client.getThreePids(); - httpBackend.when("GET", "/account/3pid").respond(200, response); - await httpBackend.flush(); + const prom = client!.getThreePids(); + httpBackend!.when("GET", "/account/3pid").respond(200, response); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -958,9 +1038,9 @@ describe("MatrixClient", function() { describe("deleteAlias", () => { it("should hit the expected API endpoint", async () => { const response = {}; - const prom = client.deleteAlias("#foo:bar"); - httpBackend.when("DELETE", "/directory/room/" + encodeURIComponent("#foo:bar")).respond(200, response); - await httpBackend.flush(); + const prom = client!.deleteAlias("#foo:bar"); + httpBackend!.when("DELETE", "/directory/room/" + encodeURIComponent("#foo:bar")).respond(200, response); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -968,10 +1048,10 @@ describe("MatrixClient", function() { describe("deleteRoomTag", () => { it("should hit the expected API endpoint", async () => { const response = {}; - const prom = client.deleteRoomTag("!roomId:server", "u.tag"); + const prom = client!.deleteRoomTag("!roomId:server", "u.tag"); const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags/u.tag`; - httpBackend.when("DELETE", url).respond(200, response); - await httpBackend.flush(); + httpBackend!.when("DELETE", url).respond(200, response); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -986,10 +1066,10 @@ describe("MatrixClient", function() { }, }; - const prom = client.getRoomTags("!roomId:server"); + const prom = client!.getRoomTags("!roomId:server"); const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags`; - httpBackend.when("GET", url).respond(200, response); - await httpBackend.flush(); + httpBackend!.when("GET", url).respond(200, response); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -1001,19 +1081,19 @@ describe("MatrixClient", function() { submit_url: "https://foobar.matrix/_matrix/matrix", }; - httpBackend.when("GET", "/_matrix/client/versions").respond(200, { + httpBackend!.when("GET", "/_matrix/client/versions").respond(200, { versions: ["r0.6.0"], }); - const prom = client.requestRegisterEmailToken("bob@email", "secret", 1); - httpBackend.when("POST", "/register/email/requestToken").check(req => { + const prom = client!.requestRegisterEmailToken("bob@email", "secret", 1); + httpBackend!.when("POST", "/register/email/requestToken").check(req => { expect(req.data).toStrictEqual({ email: "bob@email", client_secret: "secret", send_attempt: 1, }); }).respond(200, response); - await httpBackend.flush(); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -1022,11 +1102,11 @@ describe("MatrixClient", function() { it("should supply an id_access_token", async () => { const targetEmail = "gerald@example.org"; - httpBackend.when("GET", "/_matrix/client/versions").respond(200, { + httpBackend!.when("GET", "/_matrix/client/versions").respond(200, { versions: ["r0.6.0"], }); - httpBackend.when("POST", "/invite").check(req => { + httpBackend!.when("POST", "/invite").check(req => { expect(req.data).toStrictEqual({ id_server: idServerDomain, id_access_token: identityAccessToken, @@ -1035,8 +1115,8 @@ describe("MatrixClient", function() { }); }).respond(200, {}); - const prom = client.inviteByThreePid("!room:example.org", "email", targetEmail); - await httpBackend.flush(); + const prom = client!.inviteByThreePid("!room:example.org", "email", targetEmail); + await httpBackend!.flush(''); await prom; // returns empty object, so no validation needed }); }); @@ -1056,11 +1136,11 @@ describe("MatrixClient", function() { }], }; - httpBackend.when("GET", "/_matrix/client/versions").respond(200, { + httpBackend!.when("GET", "/_matrix/client/versions").respond(200, { versions: ["r0.6.0"], }); - httpBackend.when("POST", "/createRoom").check(req => { + httpBackend!.when("POST", "/createRoom").check(req => { expect(req.data).toMatchObject({ invite_3pid: expect.arrayContaining([{ ...input.invite_3pid[0], @@ -1070,8 +1150,8 @@ describe("MatrixClient", function() { expect(req.data.invite_3pid.length).toBe(1); }).respond(200, response); - const prom = client.createRoom(input); - await httpBackend.flush(); + const prom = client!.createRoom(input); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); @@ -1079,22 +1159,22 @@ describe("MatrixClient", function() { describe("requestLoginToken", () => { it("should hit the expected API endpoint with UIA", async () => { const response = {}; - const uiaData = { foo: "baa" }; - const prom = client.requestLoginToken(uiaData); - httpBackend + const uiaData = {}; + const prom = client!.requestLoginToken(uiaData); + httpBackend! .when("POST", "/unstable/org.matrix.msc3882/login/token", { auth: uiaData }) .respond(200, response); - await httpBackend.flush(); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); it("should hit the expected API endpoint without UIA", async () => { const response = {}; - const prom = client.requestLoginToken(); - httpBackend + const prom = client!.requestLoginToken(); + httpBackend! .when("POST", "/unstable/org.matrix.msc3882/login/token", {}) .respond(200, response); - await httpBackend.flush(); + await httpBackend!.flush(''); expect(await prom).toStrictEqual(response); }); }); diff --git a/spec/integ/matrix-client-opts.spec.js b/spec/integ/matrix-client-opts.spec.ts similarity index 93% rename from spec/integ/matrix-client-opts.spec.js rename to spec/integ/matrix-client-opts.spec.ts index 8e342b259..714030074 100644 --- a/spec/integ/matrix-client-opts.spec.js +++ b/spec/integ/matrix-client-opts.spec.ts @@ -5,10 +5,12 @@ import { MatrixClient } from "../../src/matrix"; import { MatrixScheduler } from "../../src/scheduler"; import { MemoryStore } from "../../src/store/memory"; import { MatrixError } from "../../src/http-api"; +import { ICreateClientOpts } from "../../src/client"; +import { IStore } from "../../src/store"; describe("MatrixClient opts", function() { const baseUrl = "http://localhost.or.something"; - let httpBackend = null; + let httpBackend = new HttpBackend(); const userId = "@alice:localhost"; const userB = "@bob:localhost"; const accessToken = "aseukfgwef"; @@ -67,7 +69,7 @@ describe("MatrixClient opts", function() { let client; beforeEach(function() { client = new MatrixClient({ - request: httpBackend.requestFn, + request: httpBackend.requestFn as unknown as ICreateClientOpts['request'], store: undefined, baseUrl: baseUrl, userId: userId, @@ -99,7 +101,7 @@ describe("MatrixClient opts", function() { ]; client.on("event", function(event) { expect(expectedEventTypes.indexOf(event.getType())).not.toEqual( - -1, "Recv unexpected event type: " + event.getType(), + -1, ); expectedEventTypes.splice( expectedEventTypes.indexOf(event.getType()), 1, @@ -118,7 +120,7 @@ describe("MatrixClient opts", function() { utils.syncPromise(client), ]); expect(expectedEventTypes.length).toEqual( - 0, "Expected to see event types: " + expectedEventTypes, + 0, ); }); }); @@ -127,8 +129,8 @@ describe("MatrixClient opts", function() { let client; beforeEach(function() { client = new MatrixClient({ - request: httpBackend.requestFn, - store: new MemoryStore(), + request: httpBackend.requestFn as unknown as ICreateClientOpts['request'], + store: new MemoryStore() as IStore, baseUrl: baseUrl, userId: userId, accessToken: accessToken, @@ -146,7 +148,7 @@ describe("MatrixClient opts", function() { error: "Ruh roh", })); client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) { - expect(false).toBe(true, "sendTextMessage resolved but shouldn't"); + expect(false).toBe(true); }, function(err) { expect(err.errcode).toEqual("M_SOMETHING"); done(); diff --git a/spec/integ/matrix-client-retrying.spec.ts b/spec/integ/matrix-client-retrying.spec.ts index 31354b89a..877e80ac9 100644 --- a/spec/integ/matrix-client-retrying.spec.ts +++ b/spec/integ/matrix-client-retrying.spec.ts @@ -14,22 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventStatus, RoomEvent, MatrixClient } from "../../src/matrix"; -import { MatrixScheduler } from "../../src/scheduler"; +import HttpBackend from "matrix-mock-request"; + +import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix"; import { Room } from "../../src/models/room"; import { TestClient } from "../TestClient"; describe("MatrixClient retrying", function() { - let client: MatrixClient = null; - let httpBackend: TestClient["httpBackend"] = null; - let scheduler; const userId = "@alice:localhost"; const accessToken = "aseukfgwef"; const roomId = "!room:here"; - let room: Room; + let client: MatrixClient | undefined; + let httpBackend: HttpBackend | undefined; + let room: Room | undefined; - beforeEach(function() { - scheduler = new MatrixScheduler(); + const setupTests = (): [MatrixClient, HttpBackend, Room] => { + const scheduler = new MatrixScheduler(); const testClient = new TestClient( userId, "DEVICE", @@ -37,15 +37,21 @@ describe("MatrixClient retrying", function() { undefined, { scheduler }, ); - httpBackend = testClient.httpBackend; - client = testClient.client; - room = new Room(roomId, client, userId); - client.store.storeRoom(room); + const httpBackend = testClient.httpBackend; + const client = testClient.client; + const room = new Room(roomId, client, userId); + client!.store.storeRoom(room); + + return [client, httpBackend, room]; + }; + + beforeEach(function() { + [client, httpBackend, room] = setupTests(); }); afterEach(function() { - httpBackend.verifyNoOutstandingExpectation(); - return httpBackend.stop(); + httpBackend!.verifyNoOutstandingExpectation(); + return httpBackend!.stop(); }); xit("should retry according to MatrixScheduler.retryFn", function() { @@ -66,7 +72,7 @@ describe("MatrixClient retrying", function() { it("should mark events as EventStatus.CANCELLED when cancelled", function() { // send a couple of events; the second will be queued - const p1 = client.sendMessage(roomId, { + const p1 = client!.sendMessage(roomId, { "msgtype": "m.text", "body": "m1", }).then(function() { @@ -79,13 +85,13 @@ describe("MatrixClient retrying", function() { // XXX: it turns out that the promise returned by this message // never gets resolved. // https://github.com/matrix-org/matrix-js-sdk/issues/496 - client.sendMessage(roomId, { + client!.sendMessage(roomId, { "msgtype": "m.text", "body": "m2", }); // both events should be in the timeline at this point - const tl = room.getLiveTimeline().getEvents(); + const tl = room!.getLiveTimeline().getEvents(); expect(tl.length).toEqual(2); const ev1 = tl[0]; const ev2 = tl[1]; @@ -94,24 +100,24 @@ describe("MatrixClient retrying", function() { expect(ev2.status).toEqual(EventStatus.SENDING); // the first message should get sent, and the second should get queued - httpBackend.when("PUT", "/send/m.room.message/").check(function() { + httpBackend!.when("PUT", "/send/m.room.message/").check(function() { // ev2 should now have been queued expect(ev2.status).toEqual(EventStatus.QUEUED); // now we can cancel the second and check everything looks sane - client.cancelPendingEvent(ev2); + client!.cancelPendingEvent(ev2); expect(ev2.status).toEqual(EventStatus.CANCELLED); expect(tl.length).toEqual(1); // shouldn't be able to cancel the first message yet expect(function() { - client.cancelPendingEvent(ev1); + client!.cancelPendingEvent(ev1); }).toThrow(); }).respond(400); // fail the first message // wait for the localecho of ev1 to be updated const p3 = new Promise((resolve, reject) => { - room.on(RoomEvent.LocalEchoUpdated, (ev0) => { + room!.on(RoomEvent.LocalEchoUpdated, (ev0) => { if (ev0 === ev1) { resolve(); } @@ -121,7 +127,7 @@ describe("MatrixClient retrying", function() { expect(tl.length).toEqual(1); // cancel the first message - client.cancelPendingEvent(ev1); + client!.cancelPendingEvent(ev1); expect(ev1.status).toEqual(EventStatus.CANCELLED); expect(tl.length).toEqual(0); }); @@ -129,7 +135,7 @@ describe("MatrixClient retrying", function() { return Promise.all([ p1, p3, - httpBackend.flushAllExpected(), + httpBackend!.flushAllExpected(), ]); }); diff --git a/spec/integ/matrix-client-room-timeline.spec.js b/spec/integ/matrix-client-room-timeline.spec.ts similarity index 76% rename from spec/integ/matrix-client-room-timeline.spec.js rename to spec/integ/matrix-client-room-timeline.spec.ts index acf751a8c..48ecee32b 100644 --- a/spec/integ/matrix-client-room-timeline.spec.js +++ b/spec/integ/matrix-client-room-timeline.spec.ts @@ -1,16 +1,35 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import HttpBackend from "matrix-mock-request"; + import * as utils from "../test-utils/test-utils"; import { EventStatus } from "../../src/models/event"; -import { RoomEvent } from "../../src"; +import { ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src"; import { TestClient } from "../TestClient"; describe("MatrixClient room timelines", function() { - let client = null; - let httpBackend = null; const userId = "@alice:localhost"; const userName = "Alice"; const accessToken = "aseukfgwef"; const roomId = "!foo:bar"; const otherUserId = "@bob:localhost"; + let client: MatrixClient | undefined; + let httpBackend: HttpBackend | undefined; + const USER_MEMBERSHIP_EVENT = utils.mkMembership({ room: roomId, mship: "join", user: userId, name: userName, }); @@ -55,8 +74,7 @@ describe("MatrixClient room timelines", function() { }, }; - function setNextSyncData(events) { - events = events || []; + function setNextSyncData(events: Partial[] = []) { NEXT_SYNC_DATA = { next_batch: "n", presence: { events: [] }, @@ -77,19 +95,9 @@ describe("MatrixClient room timelines", function() { throw new Error("setNextSyncData only works with one room id"); } if (e.state_key) { - if (e.__prev_event === undefined) { - throw new Error( - "setNextSyncData needs the prev state set to '__prev_event' " + - "for " + e.type, - ); - } - if (e.__prev_event !== null) { - // push the previous state for this event type - NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event); - } // push the current NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e); - } else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) { + } else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) { NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e); } else { NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e); @@ -97,7 +105,7 @@ describe("MatrixClient room timelines", function() { }); } - beforeEach(async function() { + const setupTestClient = (): [MatrixClient, HttpBackend] => { // these tests should work with or without timelineSupport const testClient = new TestClient( userId, @@ -106,41 +114,46 @@ describe("MatrixClient room timelines", function() { undefined, { timelineSupport: true }, ); - httpBackend = testClient.httpBackend; - client = testClient.client; + const httpBackend = testClient.httpBackend; + const client = testClient.client; setNextSyncData(); - httpBackend.when("GET", "/versions").respond(200, {}); - httpBackend.when("GET", "/pushrules").respond(200, {}); - httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); - httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); - httpBackend.when("GET", "/sync").respond(200, function() { + httpBackend!.when("GET", "/versions").respond(200, {}); + httpBackend!.when("GET", "/pushrules").respond(200, {}); + httpBackend!.when("POST", "/filter").respond(200, { filter_id: "fid" }); + httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA); + httpBackend!.when("GET", "/sync").respond(200, function() { return NEXT_SYNC_DATA; }); - client.startClient(); + client!.startClient(); + return [client!, httpBackend]; + }; + + beforeEach(async function() { + [client!, httpBackend] = setupTestClient(); await httpBackend.flush("/versions"); await httpBackend.flush("/pushrules"); await httpBackend.flush("/filter"); }); afterEach(function() { - httpBackend.verifyNoOutstandingExpectation(); - client.stopClient(); - return httpBackend.stop(); + httpBackend!.verifyNoOutstandingExpectation(); + client!.stopClient(); + return httpBackend!.stop(); }); describe("local echo events", function() { it("should be added immediately after calling MatrixClient.sendEvent " + "with EventStatus.SENDING and the right event.sender", function(done) { - client.on("sync", function(state) { + client!.on(ClientEvent.Sync, function(state) { if (state !== "PREPARED") { return; } - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; expect(room.timeline.length).toEqual(1); - client.sendTextMessage(roomId, "I am a fish", "txn1"); + client!.sendTextMessage(roomId, "I am a fish", "txn1"); // check it was added expect(room.timeline.length).toEqual(2); // check status @@ -150,68 +163,68 @@ describe("MatrixClient room timelines", function() { expect(member.userId).toEqual(userId); expect(member.name).toEqual(userName); - httpBackend.flush("/sync", 1).then(function() { + httpBackend!.flush("/sync", 1).then(function() { done(); }); }); - httpBackend.flush("/sync", 1); + httpBackend!.flush("/sync", 1); }); it("should be updated correctly when the send request finishes " + "BEFORE the event comes down the event stream", function(done) { const eventId = "$foo:bar"; - httpBackend.when("PUT", "/txn1").respond(200, { + httpBackend!.when("PUT", "/txn1").respond(200, { event_id: eventId, }); const ev = utils.mkMessage({ - body: "I am a fish", user: userId, room: roomId, + msg: "I am a fish", user: userId, room: roomId, }); ev.event_id = eventId; ev.unsigned = { transaction_id: "txn1" }; setNextSyncData([ev]); - client.on("sync", function(state) { + client!.on(ClientEvent.Sync, function(state) { if (state !== "PREPARED") { return; } - const room = client.getRoom(roomId); - client.sendTextMessage(roomId, "I am a fish", "txn1").then( - function() { - expect(room.timeline[1].getId()).toEqual(eventId); - httpBackend.flush("/sync", 1).then(function() { + const room = client!.getRoom(roomId)!; + client!.sendTextMessage(roomId, "I am a fish", "txn1").then( + function() { expect(room.timeline[1].getId()).toEqual(eventId); - done(); + httpBackend!.flush("/sync", 1).then(function() { + expect(room.timeline[1].getId()).toEqual(eventId); + done(); + }); }); - }); - httpBackend.flush("/txn1", 1); + httpBackend!.flush("/txn1", 1); }); - httpBackend.flush("/sync", 1); + httpBackend!.flush("/sync", 1); }); it("should be updated correctly when the send request finishes " + "AFTER the event comes down the event stream", function(done) { const eventId = "$foo:bar"; - httpBackend.when("PUT", "/txn1").respond(200, { + httpBackend!.when("PUT", "/txn1").respond(200, { event_id: eventId, }); const ev = utils.mkMessage({ - body: "I am a fish", user: userId, room: roomId, + msg: "I am a fish", user: userId, room: roomId, }); ev.event_id = eventId; ev.unsigned = { transaction_id: "txn1" }; setNextSyncData([ev]); - client.on("sync", function(state) { + client!.on(ClientEvent.Sync, function(state) { if (state !== "PREPARED") { return; } - const room = client.getRoom(roomId); - const promise = client.sendTextMessage(roomId, "I am a fish", "txn1"); - httpBackend.flush("/sync", 1).then(function() { + const room = client!.getRoom(roomId)!; + const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1"); + httpBackend!.flush("/sync", 1).then(function() { expect(room.timeline.length).toEqual(2); - httpBackend.flush("/txn1", 1); + httpBackend!.flush("/txn1", 1); promise.then(function() { expect(room.timeline.length).toEqual(2); expect(room.timeline[1].getId()).toEqual(eventId); @@ -219,7 +232,7 @@ describe("MatrixClient room timelines", function() { }); }); }); - httpBackend.flush("/sync", 1); + httpBackend!.flush("/sync", 1); }); }); @@ -229,7 +242,7 @@ describe("MatrixClient room timelines", function() { beforeEach(function() { sbEvents = []; - httpBackend.when("GET", "/messages").respond(200, function() { + httpBackend!.when("GET", "/messages").respond(200, function() { return { chunk: sbEvents, start: "pagin_start", @@ -240,26 +253,26 @@ describe("MatrixClient room timelines", function() { it("should set Room.oldState.paginationToken to null at the start" + " of the timeline.", function(done) { - client.on("sync", function(state) { + client!.on(ClientEvent.Sync, function(state) { if (state !== "PREPARED") { return; } - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; expect(room.timeline.length).toEqual(1); - client.scrollback(room).then(function() { + client!.scrollback(room).then(function() { expect(room.timeline.length).toEqual(1); expect(room.oldState.paginationToken).toBe(null); // still have a sync to flush - httpBackend.flush("/sync", 1).then(() => { + httpBackend!.flush("/sync", 1).then(() => { done(); }); }); - httpBackend.flush("/messages", 1); + httpBackend!.flush("/messages", 1); }); - httpBackend.flush("/sync", 1); + httpBackend!.flush("/sync", 1); }); it("should set the right event.sender values", function(done) { @@ -275,7 +288,7 @@ describe("MatrixClient room timelines", function() { // make an m.room.member event for alice's join const joinMshipEvent = utils.mkMembership({ mship: "join", user: userId, room: roomId, name: "Old Alice", - url: null, + url: undefined, }); // make an m.room.member event with prev_content for alice's nick @@ -286,7 +299,7 @@ describe("MatrixClient room timelines", function() { }); oldMshipEvent.prev_content = { displayname: "Old Alice", - avatar_url: null, + avatar_url: undefined, membership: "join", }; @@ -303,15 +316,15 @@ describe("MatrixClient room timelines", function() { joinMshipEvent, ]; - client.on("sync", function(state) { + client!.on(ClientEvent.Sync, function(state) { if (state !== "PREPARED") { return; } - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; // sync response expect(room.timeline.length).toEqual(1); - client.scrollback(room).then(function() { + client!.scrollback(room).then(function() { expect(room.timeline.length).toEqual(5); const joinMsg = room.timeline[0]; expect(joinMsg.sender.name).toEqual("Old Alice"); @@ -321,14 +334,14 @@ describe("MatrixClient room timelines", function() { expect(newMsg.sender.name).toEqual(userName); // still have a sync to flush - httpBackend.flush("/sync", 1).then(() => { + httpBackend!.flush("/sync", 1).then(() => { done(); }); }); - httpBackend.flush("/messages", 1); + httpBackend!.flush("/messages", 1); }); - httpBackend.flush("/sync", 1); + httpBackend!.flush("/sync", 1); }); it("should add it them to the right place in the timeline", function(done) { @@ -342,27 +355,27 @@ describe("MatrixClient room timelines", function() { }), ]; - client.on("sync", function(state) { + client!.on(ClientEvent.Sync, function(state) { if (state !== "PREPARED") { return; } - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; expect(room.timeline.length).toEqual(1); - client.scrollback(room).then(function() { + client!.scrollback(room).then(function() { expect(room.timeline.length).toEqual(3); expect(room.timeline[0].event).toEqual(sbEvents[1]); expect(room.timeline[1].event).toEqual(sbEvents[0]); // still have a sync to flush - httpBackend.flush("/sync", 1).then(() => { + httpBackend!.flush("/sync", 1).then(() => { done(); }); }); - httpBackend.flush("/messages", 1); + httpBackend!.flush("/messages", 1); }); - httpBackend.flush("/sync", 1); + httpBackend!.flush("/sync", 1); }); it("should use 'end' as the next pagination token", function(done) { @@ -373,25 +386,25 @@ describe("MatrixClient room timelines", function() { }), ]; - client.on("sync", function(state) { + client!.on(ClientEvent.Sync, function(state) { if (state !== "PREPARED") { return; } - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; expect(room.oldState.paginationToken).toBeTruthy(); - client.scrollback(room, 1).then(function() { + client!.scrollback(room, 1).then(function() { expect(room.oldState.paginationToken).toEqual(sbEndTok); }); - httpBackend.flush("/messages", 1).then(function() { + httpBackend!.flush("/messages", 1).then(function() { // still have a sync to flush - httpBackend.flush("/sync", 1).then(() => { + httpBackend!.flush("/sync", 1).then(() => { done(); }); }); }); - httpBackend.flush("/sync", 1); + httpBackend!.flush("/sync", 1); }); }); @@ -404,23 +417,23 @@ describe("MatrixClient room timelines", function() { setNextSyncData(eventData); return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(() => { - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; let index = 0; - client.on("Room.timeline", function(event, rm, toStart) { + client!.on(RoomEvent.Timeline, function(event, rm, toStart) { expect(toStart).toBe(false); expect(rm).toEqual(room); expect(event.event).toEqual(eventData[index]); index += 1; }); - httpBackend.flush("/messages", 1); + httpBackend!.flush("/messages", 1); return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(function() { expect(index).toEqual(2); expect(room.timeline.length).toEqual(3); @@ -442,17 +455,16 @@ describe("MatrixClient room timelines", function() { }), utils.mkMessage({ user: userId, room: roomId }), ]; - eventData[1].__prev_event = USER_MEMBERSHIP_EVENT; setNextSyncData(eventData); return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(() => { - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(function() { const preNameEvent = room.timeline[room.timeline.length - 3]; const postNameEvent = room.timeline[room.timeline.length - 1]; @@ -468,22 +480,21 @@ describe("MatrixClient room timelines", function() { name: "Room 2", }, }); - secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT; setNextSyncData([secondRoomNameEvent]); return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(() => { - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; let nameEmitCount = 0; - client.on("Room.name", function(rm) { + client!.on(RoomEvent.Name, function(rm) { nameEmitCount += 1; }); return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(function() { expect(nameEmitCount).toEqual(1); expect(room.name).toEqual("Room 2"); @@ -493,12 +504,11 @@ describe("MatrixClient room timelines", function() { name: "Room 3", }, }); - thirdRoomNameEvent.__prev_event = secondRoomNameEvent; setNextSyncData([thirdRoomNameEvent]); - httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); + httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]); }).then(function() { expect(nameEmitCount).toEqual(2); @@ -518,26 +528,24 @@ describe("MatrixClient room timelines", function() { user: userC, room: roomId, mship: "invite", skey: userD, }), ]; - eventData[0].__prev_event = null; - eventData[1].__prev_event = null; setNextSyncData(eventData); return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(() => { - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(function() { expect(room.currentState.getMembers().length).toEqual(4); - expect(room.currentState.getMember(userC).name).toEqual("C"); - expect(room.currentState.getMember(userC).membership).toEqual( + expect(room.currentState.getMember(userC)!.name).toEqual("C"); + expect(room.currentState.getMember(userC)!.membership).toEqual( "join", ); - expect(room.currentState.getMember(userD).name).toEqual(userD); - expect(room.currentState.getMember(userD).membership).toEqual( + expect(room.currentState.getMember(userD)!.name).toEqual(userD); + expect(room.currentState.getMember(userD)!.membership).toEqual( "invite", ); }); @@ -554,26 +562,26 @@ describe("MatrixClient room timelines", function() { NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true; return Promise.all([ - httpBackend.flush("/versions", 1), - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/versions", 1), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(() => { - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; - httpBackend.flush("/messages", 1); + httpBackend!.flush("/messages", 1); return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(function() { expect(room.timeline.length).toEqual(1); expect(room.timeline[0].event).toEqual(eventData[0]); expect(room.currentState.getMembers().length).toEqual(2); - expect(room.currentState.getMember(userId).name).toEqual(userName); - expect(room.currentState.getMember(userId).membership).toEqual( + expect(room.currentState.getMember(userId)!.name).toEqual(userName); + expect(room.currentState.getMember(userId)!.membership).toEqual( "join", ); - expect(room.currentState.getMember(otherUserId).name).toEqual("Bob"); - expect(room.currentState.getMember(otherUserId).membership).toEqual( + expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob"); + expect(room.currentState.getMember(otherUserId)!.membership).toEqual( "join", ); }); @@ -588,21 +596,21 @@ describe("MatrixClient room timelines", function() { NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true; return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(() => { - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; let emitCount = 0; - client.on("Room.timelineReset", function(emitRoom) { + client!.on(RoomEvent.TimelineReset, function(emitRoom) { expect(emitRoom).toEqual(room); emitCount++; }); - httpBackend.flush("/messages", 1); + httpBackend!.flush("/messages", 1); return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!), ]).then(function() { expect(emitCount).toEqual(1); }); @@ -618,7 +626,7 @@ describe("MatrixClient room timelines", function() { ]; const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` + - `${encodeURIComponent(initialSyncEventData[2].event_id)}`; + `${encodeURIComponent(initialSyncEventData[2].event_id!)}`; const contextResponse = { start: "start_token", events_before: [initialSyncEventData[1], initialSyncEventData[0]], @@ -636,19 +644,19 @@ describe("MatrixClient room timelines", function() { // Create a room from the sync await Promise.all([ - httpBackend.flushAllExpected(), - utils.syncPromise(client, 1), + httpBackend!.flushAllExpected(), + utils.syncPromise(client!, 1), ]); // Get the room after the first sync so the room is created - room = client.getRoom(roomId); + room = client!.getRoom(roomId)!; expect(room).toBeTruthy(); }); it('should clear and refresh messages in timeline', async () => { // `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()` // to construct a new timeline from. - httpBackend.when("GET", contextUrl) + httpBackend!.when("GET", contextUrl) .respond(200, function() { // The timeline should be cleared at this point in the refresh expect(room.timeline.length).toEqual(0); @@ -659,7 +667,7 @@ describe("MatrixClient room timelines", function() { // Refresh the timeline. await Promise.all([ room.refreshLiveTimeline(), - httpBackend.flushAllExpected(), + httpBackend!.flushAllExpected(), ]); // Make sure the message are visible @@ -681,7 +689,7 @@ describe("MatrixClient room timelines", function() { // middle of all of this refresh timeline logic. We want to make // sure the sync pagination still works as expected after messing // the refresh timline logic messes with the pagination tokens. - httpBackend.when("GET", contextUrl) + httpBackend!.when("GET", contextUrl) .respond(200, () => { // Now finally return and make the `/context` request respond return contextResponse; @@ -700,7 +708,7 @@ describe("MatrixClient room timelines", function() { const racingSyncEventData = [ utils.mkMessage({ user: userId, room: roomId }), ]; - const waitForRaceySyncAfterResetPromise = new Promise((resolve, reject) => { + const waitForRaceySyncAfterResetPromise = new Promise((resolve, reject) => { let eventFired = false; // Throw a more descriptive error if this part of the test times out. const failTimeout = setTimeout(() => { @@ -726,12 +734,12 @@ describe("MatrixClient room timelines", function() { // Then make a `/sync` happen by sending a message and seeing that it // shows up (simulate a /sync naturally racing with us). setNextSyncData(racingSyncEventData); - httpBackend.when("GET", "/sync").respond(200, function() { + httpBackend!.when("GET", "/sync").respond(200, function() { return NEXT_SYNC_DATA; }); await Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client, 1), + httpBackend!.flush("/sync", 1), + utils.syncPromise(client!, 1), ]); // Make sure the timeline has the racey sync data const afterRaceySyncTimelineEvents = room @@ -761,7 +769,7 @@ describe("MatrixClient room timelines", function() { await Promise.all([ refreshLiveTimelinePromise, // Then flush the remaining `/context` to left the refresh logic complete - httpBackend.flushAllExpected(), + httpBackend!.flushAllExpected(), ]); // Make sure sync pagination still works by seeing a new message show up @@ -770,12 +778,12 @@ describe("MatrixClient room timelines", function() { utils.mkMessage({ user: userId, room: roomId }), ]; setNextSyncData(afterRefreshEventData); - httpBackend.when("GET", "/sync").respond(200, function() { + httpBackend!.when("GET", "/sync").respond(200, function() { return NEXT_SYNC_DATA; }); await Promise.all([ - httpBackend.flushAllExpected(), - utils.syncPromise(client, 1), + httpBackend!.flushAllExpected(), + utils.syncPromise(client!, 1), ]); // Make sure the timeline includes the the events from the `/sync` @@ -794,7 +802,7 @@ describe("MatrixClient room timelines", function() { it('Timeline recovers after `/context` request to generate new timeline fails', async () => { // `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()` // to construct a new timeline from. - httpBackend.when("GET", contextUrl) + httpBackend!.when("GET", contextUrl) .respond(500, function() { // The timeline should be cleared at this point in the refresh expect(room.timeline.length).toEqual(0); @@ -809,7 +817,7 @@ describe("MatrixClient room timelines", function() { // Refresh the timeline and expect it to fail const settledFailedRefreshPromises = await Promise.allSettled([ room.refreshLiveTimeline(), - httpBackend.flushAllExpected(), + httpBackend!.flushAllExpected(), ]); // We only expect `TEST_FAKE_ERROR` here. Anything else is // unexpected and should fail the test. @@ -825,7 +833,7 @@ describe("MatrixClient room timelines", function() { // `/messages` request for `refreshLiveTimeline()` -> // `getLatestTimeline()` to construct a new timeline from. - httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`) + httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`) .respond(200, function() { return { chunk: [{ @@ -837,7 +845,7 @@ describe("MatrixClient room timelines", function() { // `/context` request for `refreshLiveTimeline()` -> // `getLatestTimeline()` -> `getEventTimeline()` to construct a new // timeline from. - httpBackend.when("GET", contextUrl) + httpBackend!.when("GET", contextUrl) .respond(200, function() { // The timeline should be cleared at this point in the refresh expect(room.timeline.length).toEqual(0); @@ -848,7 +856,7 @@ describe("MatrixClient room timelines", function() { // Refresh the timeline again but this time it should pass await Promise.all([ room.refreshLiveTimeline(), - httpBackend.flushAllExpected(), + httpBackend!.flushAllExpected(), ]); // Make sure sync pagination still works by seeing a new message show up @@ -857,12 +865,12 @@ describe("MatrixClient room timelines", function() { utils.mkMessage({ user: userId, room: roomId }), ]; setNextSyncData(afterRefreshEventData); - httpBackend.when("GET", "/sync").respond(200, function() { + httpBackend!.when("GET", "/sync").respond(200, function() { return NEXT_SYNC_DATA; }); await Promise.all([ - httpBackend.flushAllExpected(), - utils.syncPromise(client, 1), + httpBackend!.flushAllExpected(), + utils.syncPromise(client!, 1), ]); // Make sure the message are visible diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index f1c43c4f9..78795051c 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -16,7 +16,6 @@ limitations under the License. import 'fake-indexeddb/auto'; -import { Optional } from "matrix-events-sdk/lib/types"; import HttpBackend from "matrix-mock-request"; import { @@ -29,6 +28,11 @@ import { MatrixClient, ClientEvent, IndexedDBCryptoStore, + ISyncResponse, + IRoomEvent, + IJoinedRoom, + IStateEvent, + IMinimalEvent, NotificationCountType, } from "../../src"; import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync'; @@ -36,8 +40,6 @@ import * as utils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; describe("MatrixClient syncing", () => { - let client: Optional = null; - let httpBackend: Optional = null; const selfUserId = "@alice:localhost"; const selfAccessToken = "aseukfgwef"; const otherUserId = "@bob:localhost"; @@ -46,14 +48,21 @@ describe("MatrixClient syncing", () => { const userC = "@claire:bar"; const roomOne = "!foo:localhost"; const roomTwo = "!bar:localhost"; + let client: MatrixClient | undefined; + let httpBackend: HttpBackend | undefined; - beforeEach(() => { + const setupTestClient = (): [MatrixClient, HttpBackend] => { const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken); - httpBackend = testClient.httpBackend; - client = testClient.client; + const httpBackend = testClient.httpBackend; + const client = testClient.client; httpBackend!.when("GET", "/versions").respond(200, {}); httpBackend!.when("GET", "/pushrules").respond(200, {}); httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" }); + return [client, httpBackend]; + }; + + beforeEach(() => { + [client, httpBackend] = setupTestClient(); }); afterEach(() => { @@ -82,7 +91,7 @@ describe("MatrixClient syncing", () => { it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", (done) => { httpBackend!.when("GET", "/sync").respond(200, syncData); httpBackend!.when("GET", "/sync").check((req) => { - expect(req.queryParams.since).toEqual(syncData.next_batch); + expect(req.queryParams!.since).toEqual(syncData.next_batch); }).respond(200, syncData); client!.startClient(); @@ -93,7 +102,7 @@ describe("MatrixClient syncing", () => { }); it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => { - await client.initCrypto(); + await client!.initCrypto(); const roomId = "!cycles:example.org"; @@ -204,7 +213,7 @@ describe("MatrixClient syncing", () => { client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true); httpBackend!.when("GET", "/sync").check((req) => { - expect(JSON.parse(req.queryParams.filter).room.state.lazy_load_members).toBeTruthy(); + expect(JSON.parse(req.queryParams!.filter).room.state.lazy_load_members).toBeTruthy(); }).respond(200, syncData); client!.setGuest(false); @@ -219,7 +228,7 @@ describe("MatrixClient syncing", () => { client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true); httpBackend!.when("GET", "/sync").check((req) => { - expect(JSON.parse(req.queryParams.filter).room?.state?.lazy_load_members).toBeFalsy(); + expect(JSON.parse(req.queryParams!.filter).room?.state?.lazy_load_members).toBeFalsy(); }).respond(200, syncData); client!.setGuest(true); @@ -277,11 +286,11 @@ describe("MatrixClient syncing", () => { it("should only apply initialSyncLimit to the initial sync", () => { // 1st request httpBackend!.when("GET", "/sync").check((req) => { - expect(JSON.parse(req.queryParams.filter).room.timeline.limit).toEqual(1); + expect(JSON.parse(req.queryParams!.filter).room.timeline.limit).toEqual(1); }).respond(200, syncData); // 2nd request httpBackend!.when("GET", "/sync").check((req) => { - expect(req.queryParams.filter).toEqual("a filter id"); + expect(req.queryParams!.filter).toEqual("a filter id"); }).respond(200, syncData); client!.startClient({ initialSyncLimit: 1 }); @@ -292,7 +301,7 @@ describe("MatrixClient syncing", () => { it("should not apply initialSyncLimit to a first sync if we have a stored token", () => { httpBackend!.when("GET", "/sync").check((req) => { - expect(req.queryParams.filter).toEqual("a filter id"); + expect(req.queryParams!.filter).toEqual("a filter id"); }).respond(200, syncData); client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token"); @@ -303,26 +312,29 @@ describe("MatrixClient syncing", () => { }); describe("resolving invites to profile info", () => { - const syncData = { + const syncData: ISyncResponse = { + account_data: { + events: [], + }, next_batch: "s_5_3", presence: { events: [], }, rooms: { - join: { - - }, + join: {}, + invite: {}, + leave: {}, }, }; beforeEach(() => { - syncData.presence.events = []; + syncData.presence!.events = []; syncData.rooms.join[roomOne] = { timeline: { events: [ utils.mkMessage({ room: roomOne, user: otherUserId, msg: "hello", - }), + }) as IRoomEvent, ], }, state: { @@ -341,14 +353,14 @@ describe("MatrixClient syncing", () => { }), ], }, - }; + } as unknown as IJoinedRoom; }); it("should resolve incoming invites from /sync", () => { syncData.rooms.join[roomOne].state.events.push( utils.mkMembership({ room: roomOne, mship: "invite", user: userC, - }), + }) as IStateEvent, ); httpBackend!.when("GET", "/sync").respond(200, syncData); @@ -367,26 +379,26 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(), ]).then(() => { - const member = client!.getRoom(roomOne).getMember(userC); + const member = client!.getRoom(roomOne)!.getMember(userC)!; expect(member.name).toEqual("The Boss"); expect( - member.getAvatarUrl("home.server.url", null, null, null, false, false), + member.getAvatarUrl("home.server.url", 1, 1, '', false, false), ).toBeTruthy(); }); }); it("should use cached values from m.presence wherever possible", () => { - syncData.presence.events = [ + syncData.presence!.events = [ utils.mkPresence({ user: userC, presence: "online", name: "The Ghost", - }), + }) as IMinimalEvent, ]; syncData.rooms.join[roomOne].state.events.push( utils.mkMembership({ room: roomOne, mship: "invite", user: userC, - }), + }) as IStateEvent, ); httpBackend!.when("GET", "/sync").respond(200, syncData); @@ -399,28 +411,28 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(), ]).then(() => { - const member = client!.getRoom(roomOne).getMember(userC); + const member = client!.getRoom(roomOne)!.getMember(userC)!; expect(member.name).toEqual("The Ghost"); }); }); it("should result in events on the room member firing", () => { - syncData.presence.events = [ + syncData.presence!.events = [ utils.mkPresence({ user: userC, presence: "online", name: "The Ghost", - }), + }) as IMinimalEvent, ]; syncData.rooms.join[roomOne].state.events.push( utils.mkMembership({ room: roomOne, mship: "invite", user: userC, - }), + }) as IStateEvent, ); httpBackend!.when("GET", "/sync").respond(200, syncData); - let latestFiredName = null; + let latestFiredName: string; client!.on(RoomMemberEvent.Name, (event, m) => { if (m.userId === userC && m.roomId === roomOne) { latestFiredName = m.name; @@ -443,7 +455,7 @@ describe("MatrixClient syncing", () => { syncData.rooms.join[roomOne].state.events.push( utils.mkMembership({ room: roomOne, mship: "invite", user: userC, - }), + }) as IStateEvent, ); httpBackend!.when("GET", "/sync").respond(200, syncData); @@ -454,10 +466,10 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(), ]).then(() => { - const member = client!.getRoom(roomOne).getMember(userC); + const member = client!.getRoom(roomOne)!.getMember(userC)!; expect(member.name).toEqual(userC); expect( - member.getAvatarUrl("home.server.url", null, null, null, false, false), + member.getAvatarUrl("home.server.url", 1, 1, '', false, false), ).toBe(null); }); }); @@ -489,8 +501,8 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(), ]).then(() => { - expect(client!.getUser(userA).presence).toEqual("online"); - expect(client!.getUser(userB).presence).toEqual("unavailable"); + expect(client!.getUser(userA)!.presence).toEqual("online"); + expect(client!.getUser(userB)!.presence).toEqual("unavailable"); }); }); }); @@ -611,7 +623,7 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(2), ]).then(() => { - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; // should have clobbered the name to the one from /events expect(room.name).toEqual( nextSyncData.rooms.join[roomOne].state.events[0].content.name, @@ -629,7 +641,7 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(2), ]).then(() => { - const room = client!.getRoom(roomTwo); + const room = client!.getRoom(roomTwo)!; // should have added the message from /events expect(room.timeline.length).toEqual(2); expect(room.timeline[1].getContent().body).toEqual(msgText); @@ -645,7 +657,7 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(2), ]).then(() => { - const room = client!.getRoom(roomTwo); + const room = client!.getRoom(roomTwo)!; // should use the display name of the other person. expect(room.name).toEqual(otherDisplayName); }); @@ -661,11 +673,11 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(2), ]).then(() => { - const room = client!.getRoom(roomTwo); - let member = room.getMember(otherUserId); + const room = client!.getRoom(roomTwo)!; + let member = room.getMember(otherUserId)!; expect(member).toBeTruthy(); expect(member.typing).toEqual(true); - member = room.getMember(selfUserId); + member = room.getMember(selfUserId)!; expect(member).toBeTruthy(); expect(member.typing).toEqual(false); }); @@ -684,7 +696,7 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(2), ]).then(() => { - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; const stateAtStart = room.getLiveTimeline().getState( EventTimeline.BACKWARDS, ); @@ -782,7 +794,7 @@ describe("MatrixClient syncing", () => { awaitSyncEvent(2), ]); - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; expect(room.getTimelineNeedsRefresh()).toEqual(false); }); @@ -852,7 +864,7 @@ describe("MatrixClient syncing", () => { awaitSyncEvent(), ]); - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; expect(room.getTimelineNeedsRefresh()).toEqual(false); }); @@ -882,7 +894,7 @@ describe("MatrixClient syncing", () => { awaitSyncEvent(), ]); - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; expect(room.getTimelineNeedsRefresh()).toEqual(false); }); @@ -915,7 +927,7 @@ describe("MatrixClient syncing", () => { awaitSyncEvent(), ]); - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; expect(room.getTimelineNeedsRefresh()).toEqual(false); }); @@ -949,7 +961,7 @@ describe("MatrixClient syncing", () => { ]); // Get the room after the first sync so the room is created - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; let emitCount = 0; room.on(RoomEvent.HistoryImportedWithinTimeline, (markerEvent, room) => { @@ -1005,7 +1017,7 @@ describe("MatrixClient syncing", () => { awaitSyncEvent(2), ]); - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; expect(room.getTimelineNeedsRefresh()).toEqual(true); }); }); @@ -1060,7 +1072,7 @@ describe("MatrixClient syncing", () => { ]); // Get the room after the first sync so the room is created - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; expect(room).toBeTruthy(); let stateEventEmitCount = 0; @@ -1134,7 +1146,7 @@ describe("MatrixClient syncing", () => { ]); // Get the room after the first sync so the room is created - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; expect(room).toBeTruthy(); let stateEventEmitCount = 0; @@ -1231,7 +1243,7 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(), ]).then(() => { - const room = client!.getRoom(roomTwo); + const room = client!.getRoom(roomTwo)!; expect(room).toBeTruthy(); const tok = room.getLiveTimeline() .getPaginationToken(EventTimeline.BACKWARDS); @@ -1274,7 +1286,7 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(), ]).then(() => { - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; const tl = room.getLiveTimeline(); expect(tl.getEvents().length).toEqual(1); expect(resetCallCount).toEqual(1); @@ -1353,7 +1365,7 @@ describe("MatrixClient syncing", () => { httpBackend!.flushAllExpected(), awaitSyncEvent(), ]).then(() => { - const room = client!.getRoom(roomOne); + const room = client!.getRoom(roomOne)!; expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{ type: "m.read", userId: userC, @@ -1426,8 +1438,8 @@ describe("MatrixClient syncing", () => { ]).then(() => { const room = client!.getRoom(roomOne); - expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5); - expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2); + expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5); + expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2); }); }); }); @@ -1469,7 +1481,7 @@ describe("MatrixClient syncing", () => { const prom = new Promise((resolve) => { httpBackend!.when("GET", "/sync").check((req) => { - expect(req.queryParams.filter).toEqual("another_id"); + expect(req.queryParams!.filter).toEqual("another_id"); resolve(); }).respond(200, {}); }); @@ -1514,7 +1526,7 @@ describe("MatrixClient syncing", () => { return Promise.all([ client!.syncLeftRooms().then(() => { - const room = client!.getRoom(roomTwo); + const room = client!.getRoom(roomTwo)!; const tok = room.getLiveTimeline().getPaginationToken( EventTimeline.BACKWARDS); @@ -1536,7 +1548,7 @@ describe("MatrixClient syncing", () => { * @returns {Promise} promise which resolves after the sync events have happened */ function awaitSyncEvent(numSyncs?: number) { - return utils.syncPromise(client, numSyncs); + return utils.syncPromise(client!, numSyncs); } }); diff --git a/spec/integ/megolm-backup.spec.ts b/spec/integ/megolm-backup.spec.ts index 5fa675519..492e4f1dc 100644 --- a/spec/integ/megolm-backup.spec.ts +++ b/spec/integ/megolm-backup.spec.ts @@ -95,26 +95,31 @@ describe("megolm key backups", function() { return; } const Olm = global.Olm; - - let testOlmAccount: Account; + let testOlmAccount: Olm.Account; let aliceTestClient: TestClient; + const setupTestClient = (): [Account, TestClient] => { + const aliceTestClient = new TestClient( + "@alice:localhost", "xzcvb", "akjgkrgjs", + ); + const testOlmAccount = new Olm.Account(); + testOlmAccount!.create(); + + return [testOlmAccount, aliceTestClient]; + }; + beforeAll(function() { return Olm.init(); }); beforeEach(async function() { - aliceTestClient = new TestClient( - "@alice:localhost", "xzcvb", "akjgkrgjs", - ); - testOlmAccount = new Olm.Account(); - testOlmAccount.create(); - await aliceTestClient.client.initCrypto(); - aliceTestClient.client.crypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO; + [testOlmAccount, aliceTestClient] = setupTestClient(); + await aliceTestClient!.client.initCrypto(); + aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO; }); afterEach(function() { - return aliceTestClient.stop(); + return aliceTestClient!.stop(); }); it("Alice checks key backups when receiving a message she can't decrypt", function() { @@ -130,22 +135,22 @@ describe("megolm key backups", function() { }, }; - return aliceTestClient.start().then(() => { + return aliceTestClient!.start().then(() => { return createOlmSession(testOlmAccount, aliceTestClient); }).then(() => { const privkey = decodeRecoveryKey(RECOVERY_KEY); - return aliceTestClient.client.crypto.storeSessionBackupPrivateKey(privkey); + return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey); }).then(() => { - aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); - aliceTestClient.expectKeyBackupQuery( + aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse); + aliceTestClient!.expectKeyBackupQuery( ROOM_ID, SESSION_ID, 200, CURVE25519_KEY_BACKUP_DATA, ); - return aliceTestClient.httpBackend.flushAllExpected(); + return aliceTestClient!.httpBackend.flushAllExpected(); }).then(function(): Promise { - const room = aliceTestClient.client.getRoom(ROOM_ID); + const room = aliceTestClient!.client.getRoom(ROOM_ID)!; const event = room.getLiveTimeline().getEvents()[0]; if (event.getContent()) { diff --git a/spec/integ/megolm-integ.spec.ts b/spec/integ/megolm-integ.spec.ts index ae2771f3b..9454749a9 100644 --- a/spec/integ/megolm-integ.spec.ts +++ b/spec/integ/megolm-integ.spec.ts @@ -207,9 +207,11 @@ describe("megolm", () => { } const Olm = global.Olm; - let testOlmAccount: Olm.Account; - let testSenderKey: string; - let aliceTestClient: TestClient; + let testOlmAccount = {} as unknown as Olm.Account; + let testSenderKey = ''; + let aliceTestClient = new TestClient( + "@alice:localhost", "device2", "access_token2", + ); /** * Get the device keys for testOlmAccount in a format suitable for a @@ -283,12 +285,12 @@ describe("megolm", () => { it("Alice receives a megolm message", async () => { await aliceTestClient.start(); - aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); - aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz"; + aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz"; // make the room_key event const roomKeyEncrypted = encryptGroupSessionKey({ @@ -322,7 +324,7 @@ describe("megolm", () => { aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); await aliceTestClient.flushSync(); - const room = aliceTestClient.client.getRoom(ROOM_ID); + const room = aliceTestClient.client.getRoom(ROOM_ID)!; const event = room.getLiveTimeline().getEvents()[0]; expect(event.isEncrypted()).toBe(true); const decryptedEvent = await testUtils.awaitDecryption(event); @@ -332,12 +334,12 @@ describe("megolm", () => { it("Alice receives a megolm message before the session keys", async () => { // https://github.com/vector-im/element-web/issues/2273 await aliceTestClient.start(); - aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); - aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz"; + aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz"; // make the room_key event, but don't send it yet const roomKeyEncrypted = encryptGroupSessionKey({ @@ -362,7 +364,7 @@ describe("megolm", () => { }); await aliceTestClient.flushSync(); - const room = aliceTestClient.client.getRoom(ROOM_ID); + const room = aliceTestClient.client.getRoom(ROOM_ID)!; expect(room.getLiveTimeline().getEvents()[0].getContent().msgtype).toEqual('m.bad.encrypted'); // now she gets the room_key event @@ -392,12 +394,12 @@ describe("megolm", () => { it("Alice gets a second room_key message", async () => { await aliceTestClient.start(); - aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); - aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz"; + aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz"; // make the room_key event const roomKeyEncrypted1 = encryptGroupSessionKey({ @@ -451,7 +453,7 @@ describe("megolm", () => { await aliceTestClient.flushSync(); await aliceTestClient.flushSync(); - const room = aliceTestClient.client.getRoom(ROOM_ID); + const room = aliceTestClient.client.getRoom(ROOM_ID)!; await room.decryptCriticalEvents(); const event = room.getLiveTimeline().getEvents()[0]; expect(event.getContent().body).toEqual('42'); @@ -499,7 +501,7 @@ describe("megolm", () => { let inboundGroupSession: Olm.InboundGroupSession; aliceTestClient.httpBackend.when( 'PUT', '/sendToDevice/m.room.encrypted/', - ).respond(200, function(_path, content) { + ).respond(200, function(_path, content: any) { const m = content.messages['@bob:xyz'].DEVICE_ID; const ct = m.ciphertext[testSenderKey]; const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body)); @@ -525,7 +527,7 @@ describe("megolm", () => { return { event_id: '$event_id' }; }); - const room = aliceTestClient.client.getRoom(ROOM_ID); + const room = aliceTestClient.client.getRoom(ROOM_ID)!; const pendingMsg = room.getPendingEvents()[0]; await Promise.all([ @@ -628,7 +630,7 @@ describe("megolm", () => { let megolmSessionId: string; aliceTestClient.httpBackend.when( 'PUT', '/sendToDevice/m.room.encrypted/', - ).respond(200, function(_path, content) { + ).respond(200, function(_path, content: any) { logger.log('sendToDevice: ', content); const m = content.messages['@bob:xyz'].DEVICE_ID; const ct = m.ciphertext[testSenderKey]; @@ -706,7 +708,7 @@ describe("megolm", () => { // invalidate the device cache for all members in e2e rooms (ie, // herself), and do a key query. aliceTestClient.expectKeyQuery( - getTestKeysQueryResponse(aliceTestClient.userId), + getTestKeysQueryResponse(aliceTestClient.userId!), ); await aliceTestClient.httpBackend.flushAllExpected(); @@ -716,28 +718,30 @@ describe("megolm", () => { await aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'); throw new Error("sendTextMessage succeeded on an unknown device"); } catch (e) { - expect(e.name).toEqual("UnknownDeviceError"); - expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]); - expect(Object.keys(e.devices[aliceTestClient.userId])). + expect((e as any).name).toEqual("UnknownDeviceError"); + expect(Object.keys((e as any).devices)).toEqual([aliceTestClient.userId!]); + expect(Object.keys((e as any)?.devices[aliceTestClient.userId!])). toEqual(['DEVICE_ID']); } // mark the device as known, and resend. - aliceTestClient.client.setDeviceKnown(aliceTestClient.userId, 'DEVICE_ID'); + aliceTestClient.client.setDeviceKnown(aliceTestClient.userId!, 'DEVICE_ID'); aliceTestClient.httpBackend.when('POST', '/keys/claim').respond( - 200, function(_path, content) { - expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID) + 200, function(_path, content: IClaimOTKsResult) { + expect(content.one_time_keys[aliceTestClient.userId!].DEVICE_ID) .toEqual("signed_curve25519"); - return getTestKeysClaimResponse(aliceTestClient.userId); + return getTestKeysClaimResponse(aliceTestClient.userId!); }); let p2pSession: Olm.Session; let inboundGroupSession: Olm.InboundGroupSession; aliceTestClient.httpBackend.when( 'PUT', '/sendToDevice/m.room.encrypted/', - ).respond(200, function(_path, content) { + ).respond(200, function(_path, content: { + messages: { [userId: string]: { [deviceId: string]: Record }}; + }) { logger.log("sendToDevice: ", content); - const m = content.messages[aliceTestClient.userId].DEVICE_ID; + const m = content.messages[aliceTestClient.userId!].DEVICE_ID; const ct = m.ciphertext[testSenderKey]; expect(ct.type).toEqual(0); // pre-key message @@ -751,7 +755,7 @@ describe("megolm", () => { return {}; }); - let decrypted: IEvent; + let decrypted: Partial = {}; aliceTestClient.httpBackend.when( 'PUT', '/send/', ).respond(200, function(_path, content: IContent) { @@ -766,7 +770,7 @@ describe("megolm", () => { }); // Grab the event that we'll need to resend - const room = aliceTestClient.client.getRoom(ROOM_ID); + const room = aliceTestClient.client.getRoom(ROOM_ID)!; const pendingEvents = room.getPendingEvents(); expect(pendingEvents.length).toEqual(1); const unsentEvent = pendingEvents[0]; @@ -781,7 +785,7 @@ describe("megolm", () => { ]); expect(decrypted.type).toEqual('m.room.message'); - expect(decrypted.content.body).toEqual('test'); + expect(decrypted.content?.body).toEqual('test'); }); it('Alice should wait for device list to complete when sending a megolm message', async () => { @@ -830,11 +834,11 @@ describe("megolm", () => { it("Alice exports megolm keys and imports them to a new device", async () => { aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} }); await aliceTestClient.start(); - aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); // establish an olm session with alice const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); - aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz"; + aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz"; const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); @@ -867,7 +871,7 @@ describe("megolm", () => { }); await aliceTestClient.flushSync(); - const room = aliceTestClient.client.getRoom(ROOM_ID); + const room = aliceTestClient.client.getRoom(ROOM_ID)!; await room.decryptCriticalEvents(); expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42'); @@ -883,7 +887,7 @@ describe("megolm", () => { await aliceTestClient.client.importRoomKeys(exported); await aliceTestClient.start(); - aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz"; + aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz"; const syncResponse = { next_batch: 1, @@ -927,7 +931,7 @@ describe("megolm", () => { ...rawEvent, room: ROOM_ID, }); - await event1.attemptDecryption(testClient.client.crypto, { isRetry: true }); + await event1.attemptDecryption(testClient.client.crypto!, { isRetry: true }); expect(event1.isKeySourceUntrusted()).toBeTruthy(); const event2 = testUtils.mkEvent({ @@ -943,26 +947,26 @@ describe("megolm", () => { // @ts-ignore - private event2.senderCurve25519Key = testSenderKey; // @ts-ignore - private - testClient.client.crypto.onRoomKeyEvent(event2); + testClient.client.crypto!.onRoomKeyEvent(event2); const event3 = testUtils.mkEvent({ event: true, ...rawEvent, room: ROOM_ID, }); - await event3.attemptDecryption(testClient.client.crypto, { isRetry: true }); + await event3.attemptDecryption(testClient.client.crypto!, { isRetry: true }); expect(event3.isKeySourceUntrusted()).toBeFalsy(); testClient.stop(); }); it("Alice can decrypt a message with falsey content", async () => { await aliceTestClient.start(); - aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); - aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz"; + aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz"; // make the room_key event const roomKeyEncrypted = encryptGroupSessionKey({ @@ -1005,7 +1009,7 @@ describe("megolm", () => { aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); await aliceTestClient.flushSync(); - const room = aliceTestClient.client.getRoom(ROOM_ID); + const room = aliceTestClient.client.getRoom(ROOM_ID)!; const event = room.getLiveTimeline().getEvents()[0]; expect(event.isEncrypted()).toBe(true); const decryptedEvent = await testUtils.awaitDecryption(event); @@ -1018,12 +1022,12 @@ describe("megolm", () => { "should successfully decrypt bundled redaction events that don't include a room_id in their /sync data", async () => { await aliceTestClient.start(); - aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); - aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz"; + aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz"; // make the room_key event const roomKeyEncrypted = encryptGroupSessionKey({ @@ -1072,10 +1076,10 @@ describe("megolm", () => { aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); await aliceTestClient.flushSync(); - const room = aliceTestClient.client.getRoom(ROOM_ID); + const room = aliceTestClient.client.getRoom(ROOM_ID)!; const event = room.getLiveTimeline().getEvents()[0]; expect(event.isEncrypted()).toBe(true); - await event.attemptDecryption(aliceTestClient.client.crypto); + await event.attemptDecryption(aliceTestClient.client.crypto!); expect(event.getContent()).toEqual({}); const redactionEvent: any = event.getRedactionEvent(); expect(redactionEvent.content.reason).toEqual("redaction test"); @@ -1089,7 +1093,7 @@ describe("megolm", () => { await beccaTestClient.client.initCrypto(); await aliceTestClient.start(); - aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); + aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); await beccaTestClient.start(); const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {}); @@ -1107,7 +1111,7 @@ describe("megolm", () => { }, }); - await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom); + await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom); // remove keys from the event // @ts-ignore private properties event.clearEvent = undefined; @@ -1116,23 +1120,23 @@ describe("megolm", () => { // @ts-ignore private properties event.claimedEd25519Key = null; - const device = new DeviceInfo(beccaTestClient.client.deviceId); - aliceTestClient.client.crypto.deviceList.getDeviceByIdentityKey = () => device; - aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId(); + const device = new DeviceInfo(beccaTestClient.client.deviceId!); + aliceTestClient.client.crypto!.deviceList.getDeviceByIdentityKey = () => device; + aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId()!; // Create an olm session for Becca and Alice's devices const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload(); const aliceOtkId = Object.keys(aliceOtks)[0]; const aliceOtk = aliceOtks[aliceOtkId]; const p2pSession = new global.Olm.Session(); - await beccaTestClient.client.crypto.cryptoStore.doTxn( + await beccaTestClient.client.crypto!.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => { + beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => { const account = new global.Olm.Account(); try { - account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount); + account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount); p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key); } finally { account.free(); @@ -1142,7 +1146,7 @@ describe("megolm", () => { ); const content = event.getWireContent(); - const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey( + const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey( ROOM_ID, content.sender_key, content.session_id, @@ -1213,7 +1217,7 @@ describe("megolm", () => { }); await aliceTestClient.flushSync(); - const room = aliceTestClient.client.getRoom(ROOM_ID); + const room = aliceTestClient.client.getRoom(ROOM_ID)!; const roomEvent = room.getLiveTimeline().getEvents()[0]; expect(roomEvent.isEncrypted()).toBe(true); const decryptedEvent = await testUtils.awaitDecryption(roomEvent); @@ -1246,7 +1250,7 @@ describe("megolm", () => { }, }); - await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom); + await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom); // remove keys from the event // @ts-ignore private properties event.clearEvent = undefined; @@ -1255,22 +1259,22 @@ describe("megolm", () => { // @ts-ignore private properties event.claimedEd25519Key = null; - const device = new DeviceInfo(beccaTestClient.client.deviceId); - aliceTestClient.client.crypto.deviceList.getDeviceByIdentityKey = () => device; + const device = new DeviceInfo(beccaTestClient.client.deviceId!); + aliceTestClient.client.crypto!.deviceList.getDeviceByIdentityKey = () => device; // Create an olm session for Becca and Alice's devices const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload(); const aliceOtkId = Object.keys(aliceOtks)[0]; const aliceOtk = aliceOtks[aliceOtkId]; const p2pSession = new global.Olm.Session(); - await beccaTestClient.client.crypto.cryptoStore.doTxn( + await beccaTestClient.client.crypto!.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { - beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => { + beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => { const account = new global.Olm.Account(); try { - account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount); + account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount); p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key); } finally { account.free(); @@ -1280,7 +1284,7 @@ describe("megolm", () => { ); const content = event.getWireContent(); - const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey( + const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey( ROOM_ID, content.sender_key, content.session_id, @@ -1352,7 +1356,7 @@ describe("megolm", () => { await aliceTestClient.flushSync(); // Decryption should fail, because Alice hasn't received any keys she can trust - const room = aliceTestClient.client.getRoom(ROOM_ID); + const room = aliceTestClient.client.getRoom(ROOM_ID)!; const roomEvent = room.getLiveTimeline().getEvents()[0]; expect(roomEvent.isEncrypted()).toBe(true); const decryptedEvent = await testUtils.awaitDecryption(roomEvent); diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index f7dc68754..47e6ba8df 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -31,10 +31,10 @@ import { IStoredClientOpts } from "../../src/client"; import { logger } from "../../src/logger"; describe("SlidingSyncSdk", () => { - let client: MatrixClient = null; - let httpBackend: MockHttpBackend = null; - let sdk: SlidingSyncSdk = null; - let mockSlidingSync: SlidingSync = null; + let client: MatrixClient | undefined; + let httpBackend: MockHttpBackend | undefined; + let sdk: SlidingSyncSdk | undefined; + let mockSlidingSync: SlidingSync | undefined; const selfUserId = "@alice:localhost"; const selfAccessToken = "aseukfgwef"; @@ -66,7 +66,7 @@ describe("SlidingSyncSdk", () => { event_id: "$" + eventIdCounter, }; }; - const mkOwnStateEvent = (evType: string, content: object, stateKey?: string): IStateEvent => { + const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => { eventIdCounter++; return { type: evType, @@ -103,24 +103,24 @@ describe("SlidingSyncSdk", () => { client = testClient.client; mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0)); if (testOpts.withCrypto) { - httpBackend.when("GET", "/room_keys/version").respond(404, {}); - await client.initCrypto(); - testOpts.crypto = client.crypto; + httpBackend!.when("GET", "/room_keys/version").respond(404, {}); + await client!.initCrypto(); + testOpts.crypto = client!.crypto; } - httpBackend.when("GET", "/_matrix/client/r0/pushrules").respond(200, {}); + httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {}); sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts); }; // tear down client/httpBackend globals const teardownClient = () => { - client.stopClient(); - return httpBackend.stop(); + client!.stopClient(); + return httpBackend!.stop(); }; // find an extension on a SlidingSyncSdk instance const findExtension = (name: string): Extension => { - expect(mockSlidingSync.registerExtension).toHaveBeenCalled(); - const mockFn = mockSlidingSync.registerExtension as jest.Mock; + expect(mockSlidingSync!.registerExtension).toHaveBeenCalled(); + const mockFn = mockSlidingSync!.registerExtension as jest.Mock; // find the extension for (let i = 0; i < mockFn.mock.calls.length; i++) { const calledExtension = mockFn.mock.calls[i][0] as Extension; @@ -137,14 +137,14 @@ describe("SlidingSyncSdk", () => { }); afterAll(teardownClient); it("can sync()", async () => { - const hasSynced = sdk.sync(); - await httpBackend.flushAllExpected(); + const hasSynced = sdk!.sync(); + await httpBackend!.flushAllExpected(); await hasSynced; - expect(mockSlidingSync.start).toBeCalled(); + expect(mockSlidingSync!.start).toBeCalled(); }); it("can stop()", async () => { - sdk.stop(); - expect(mockSlidingSync.stop).toBeCalled(); + sdk!.stop(); + expect(mockSlidingSync!.stop).toBeCalled(); }); }); @@ -156,8 +156,8 @@ describe("SlidingSyncSdk", () => { describe("initial", () => { beforeAll(async () => { - const hasSynced = sdk.sync(); - await httpBackend.flushAllExpected(); + const hasSynced = sdk!.sync(); + await httpBackend!.flushAllExpected(); await hasSynced; }); // inject some rooms with different fields set. @@ -277,8 +277,8 @@ describe("SlidingSyncSdk", () => { }; it("can be created with required_state and timeline", () => { - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]); - const gotRoom = client.getRoom(roomA); + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]); + const gotRoom = client!.getRoom(roomA); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect(gotRoom.name).toEqual(data[roomA].name); @@ -287,8 +287,8 @@ describe("SlidingSyncSdk", () => { }); it("can be created with timeline only", () => { - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]); - const gotRoom = client.getRoom(roomB); + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]); + const gotRoom = client!.getRoom(roomB); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect(gotRoom.name).toEqual(data[roomB].name); @@ -297,8 +297,8 @@ describe("SlidingSyncSdk", () => { }); it("can be created with a highlight_count", () => { - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]); - const gotRoom = client.getRoom(roomC); + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]); + const gotRoom = client!.getRoom(roomC); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect( @@ -307,8 +307,8 @@ describe("SlidingSyncSdk", () => { }); it("can be created with a notification_count", () => { - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]); - const gotRoom = client.getRoom(roomD); + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]); + const gotRoom = client!.getRoom(roomD); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect( @@ -317,8 +317,8 @@ describe("SlidingSyncSdk", () => { }); it("can be created with an invited/joined_count", () => { - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]); - const gotRoom = client.getRoom(roomG); + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]); + const gotRoom = client!.getRoom(roomG); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count); @@ -326,8 +326,8 @@ describe("SlidingSyncSdk", () => { }); it("can be created with invite_state", () => { - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]); - const gotRoom = client.getRoom(roomE); + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]); + const gotRoom = client!.getRoom(roomE); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect(gotRoom.getMyMembership()).toEqual("invite"); @@ -335,8 +335,8 @@ describe("SlidingSyncSdk", () => { }); it("uses the 'name' field to caluclate the room name", () => { - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]); - const gotRoom = client.getRoom(roomF); + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]); + const gotRoom = client!.getRoom(roomF); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect( @@ -347,12 +347,12 @@ describe("SlidingSyncSdk", () => { describe("updating", () => { it("can update with a new timeline event", async () => { const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" }); - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, { + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, { timeline: [newEvent], required_state: [], name: data[roomA].name, }); - const gotRoom = client.getRoom(roomA); + const gotRoom = client!.getRoom(roomA); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } const newTimeline = data[roomA].timeline; @@ -361,31 +361,31 @@ describe("SlidingSyncSdk", () => { }); it("can update with a new required_state event", async () => { - let gotRoom = client.getRoom(roomB); + let gotRoom = client!.getRoom(roomB); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, { + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, { required_state: [ mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""), ], timeline: [], name: data[roomB].name, }); - gotRoom = client.getRoom(roomB); + gotRoom = client!.getRoom(roomB); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted); }); it("can update with a new highlight_count", async () => { - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, { + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, { name: data[roomC].name, required_state: [], timeline: [], highlight_count: 1, }); - const gotRoom = client.getRoom(roomC); + const gotRoom = client!.getRoom(roomC); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect( @@ -394,13 +394,13 @@ describe("SlidingSyncSdk", () => { }); it("can update with a new notification_count", async () => { - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, { + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, { name: data[roomD].name, required_state: [], timeline: [], notification_count: 1, }); - const gotRoom = client.getRoom(roomD); + const gotRoom = client!.getRoom(roomD); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect( @@ -409,13 +409,13 @@ describe("SlidingSyncSdk", () => { }); it("can update with a new joined_count", () => { - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, { + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, { name: data[roomD].name, required_state: [], timeline: [], joined_count: 1, }); - const gotRoom = client.getRoom(roomG); + const gotRoom = client!.getRoom(roomG); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } expect(gotRoom.getJoinedMemberCount()).toEqual(1); @@ -433,13 +433,13 @@ describe("SlidingSyncSdk", () => { mkOwnEvent(EventType.RoomMessage, { body: "old event C" }), ...timeline, ]; - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, { + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, { timeline: oldTimeline, required_state: [], name: data[roomA].name, initial: true, // e.g requested via room subscription }); - const gotRoom = client.getRoom(roomA); + const gotRoom = client!.getRoom(roomA); expect(gotRoom).toBeDefined(); if (gotRoom == null) { return; } @@ -458,50 +458,50 @@ describe("SlidingSyncSdk", () => { describe("lifecycle", () => { beforeAll(async () => { await setupClient(); - const hasSynced = sdk.sync(); - await httpBackend.flushAllExpected(); + const hasSynced = sdk!.sync(); + await httpBackend!.flushAllExpected(); await hasSynced; }); const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class... it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => { - mockSlidingSync.emit( + mockSlidingSync!.emit( SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, { pos: "h", lists: [], rooms: {}, extensions: {} }, null, ); - expect(sdk.getSyncState()).toEqual(SyncState.Syncing); + expect(sdk!.getSyncState()).toEqual(SyncState.Syncing); - mockSlidingSync.emit( + mockSlidingSync!.emit( SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"), ); - expect(sdk.getSyncState()).toEqual(SyncState.Reconnecting); + expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting); for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) { - mockSlidingSync.emit( + mockSlidingSync!.emit( SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"), ); } - expect(sdk.getSyncState()).toEqual(SyncState.Error); + expect(sdk!.getSyncState()).toEqual(SyncState.Error); }); it("emits SyncState.Syncing after a previous SyncState.Error", async () => { - mockSlidingSync.emit( + mockSlidingSync!.emit( SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, { pos: "i", lists: [], rooms: {}, extensions: {} }, null, ); - expect(sdk.getSyncState()).toEqual(SyncState.Syncing); + expect(sdk!.getSyncState()).toEqual(SyncState.Syncing); }); it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => { - expect(mockSlidingSync.stop).not.toBeCalled(); - mockSlidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({ + expect(mockSlidingSync!.stop).not.toBeCalled(); + mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({ errcode: "M_UNKNOWN_TOKEN", message: "Oh no your access token is no longer valid", })); - expect(sdk.getSyncState()).toEqual(SyncState.Error); - expect(mockSlidingSync.stop).toBeCalled(); + expect(sdk!.getSyncState()).toEqual(SyncState.Error); + expect(mockSlidingSync!.stop).toBeCalled(); }); }); @@ -517,8 +517,8 @@ describe("SlidingSyncSdk", () => { avatar_url: "mxc://foobar", displayname: "The Invitee", }; - httpBackend.when("GET", "/profile").respond(200, inviteeProfile); - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, { + httpBackend!.when("GET", "/profile").respond(200, inviteeProfile); + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, { initial: true, name: "Room with Invite", required_state: [], @@ -529,10 +529,10 @@ describe("SlidingSyncSdk", () => { mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee), ], }); - await httpBackend.flush("/profile", 1, 1000); - const room = client.getRoom(roomId); + await httpBackend!.flush("/profile", 1, 1000); + const room = client!.getRoom(roomId)!; expect(room).toBeDefined(); - const inviteeMember = room.getMember(invitee); + const inviteeMember = room.getMember(invitee)!; expect(inviteeMember).toBeDefined(); expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url); expect(inviteeMember.name).toEqual(inviteeProfile.displayname); @@ -545,8 +545,8 @@ describe("SlidingSyncSdk", () => { await setupClient({ withCrypto: true, }); - const hasSynced = sdk.sync(); - await httpBackend.flushAllExpected(); + const hasSynced = sdk!.sync(); + await httpBackend!.flushAllExpected(); await hasSynced; ext = findExtension("e2ee"); }); @@ -554,7 +554,7 @@ describe("SlidingSyncSdk", () => { // needed else we do some async operations in the background which can cause Jest to whine: // "Cannot log after tests are done. Did you forget to wait for something async in your test?" // Attempted to log "Saving device tracking data null"." - client.crypto.stop(); + client!.crypto!.stop(); }); it("gets enabled on the initial request only", () => { expect(ext.onRequest(true)).toEqual({ @@ -572,38 +572,38 @@ describe("SlidingSyncSdk", () => { // TODO: more assertions? }); it("can update OTK counts", () => { - client.crypto.updateOneTimeKeyCount = jest.fn(); + client!.crypto!.updateOneTimeKeyCount = jest.fn(); ext.onResponse({ device_one_time_keys_count: { signed_curve25519: 42, }, }); - expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(42); + expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42); ext.onResponse({ device_one_time_keys_count: { not_signed_curve25519: 42, // missing field -> default to 0 }, }); - expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(0); + expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0); }); it("can update fallback keys", () => { ext.onResponse({ device_unused_fallback_key_types: ["signed_curve25519"], }); - expect(client.crypto.getNeedsNewFallback()).toEqual(false); + expect(client!.crypto!.getNeedsNewFallback()).toEqual(false); ext.onResponse({ device_unused_fallback_key_types: ["not_signed_curve25519"], }); - expect(client.crypto.getNeedsNewFallback()).toEqual(true); + expect(client!.crypto!.getNeedsNewFallback()).toEqual(true); }); }); describe("ExtensionAccountData", () => { let ext: Extension; beforeAll(async () => { await setupClient(); - const hasSynced = sdk.sync(); - await httpBackend.flushAllExpected(); + const hasSynced = sdk!.sync(); + await httpBackend!.flushAllExpected(); await hasSynced; ext = findExtension("account_data"); }); @@ -618,7 +618,7 @@ describe("SlidingSyncSdk", () => { const globalContent = { info: "here", }; - let globalData = client.getAccountData(globalType); + let globalData = client!.getAccountData(globalType); expect(globalData).toBeUndefined(); ext.onResponse({ global: [ @@ -628,13 +628,13 @@ describe("SlidingSyncSdk", () => { }, ], }); - globalData = client.getAccountData(globalType); + globalData = client!.getAccountData(globalType)!; expect(globalData).toBeDefined(); expect(globalData.getContent()).toEqual(globalContent); }); it("processes rooms account data", async () => { const roomId = "!room:id"; - mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, { + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, { name: "Room with account data", required_state: [], timeline: [ @@ -660,9 +660,9 @@ describe("SlidingSyncSdk", () => { ], }, }); - const room = client.getRoom(roomId); + const room = client!.getRoom(roomId)!; expect(room).toBeDefined(); - const event = room.getAccountData(roomType); + const event = room.getAccountData(roomType)!; expect(event).toBeDefined(); expect(event.getContent()).toEqual(roomContent); }); @@ -681,9 +681,9 @@ describe("SlidingSyncSdk", () => { ], }, }); - const room = client.getRoom(unknownRoomId); + const room = client!.getRoom(unknownRoomId); expect(room).toBeNull(); - expect(client.getAccountData(roomType)).toBeUndefined(); + expect(client!.getAccountData(roomType)).toBeUndefined(); }); it("can update push rules via account data", async () => { const roomId = "!foo:bar"; @@ -703,7 +703,7 @@ describe("SlidingSyncSdk", () => { }], }, }; - let pushRule = client.getRoomPushRule("global", roomId); + let pushRule = client!.getRoomPushRule("global", roomId); expect(pushRule).toBeUndefined(); ext.onResponse({ global: [ @@ -713,16 +713,16 @@ describe("SlidingSyncSdk", () => { }, ], }); - pushRule = client.getRoomPushRule("global", roomId); - expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific][0]); + pushRule = client!.getRoomPushRule("global", roomId)!; + expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]); }); }); describe("ExtensionToDevice", () => { let ext: Extension; beforeAll(async () => { await setupClient(); - const hasSynced = sdk.sync(); - await httpBackend.flushAllExpected(); + const hasSynced = sdk!.sync(); + await httpBackend!.flushAllExpected(); await hasSynced; ext = findExtension("to_device"); }); @@ -753,7 +753,7 @@ describe("SlidingSyncSdk", () => { foo: "bar", }; let called = false; - client.once(ClientEvent.ToDeviceEvent, (ev) => { + client!.once(ClientEvent.ToDeviceEvent, (ev) => { expect(ev.getContent()).toEqual(toDeviceContent); expect(ev.getType()).toEqual(toDeviceType); called = true; @@ -771,7 +771,7 @@ describe("SlidingSyncSdk", () => { }); it("can cancel key verification requests", async () => { const seen: Record = {}; - client.on(ClientEvent.ToDeviceEvent, (ev) => { + client!.on(ClientEvent.ToDeviceEvent, (ev) => { const evType = ev.getType(); expect(seen[evType]).toBeFalsy(); seen[evType] = true; diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 4cfe39215..0364bed0f 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -30,8 +30,8 @@ import { sleep } from "../../src/utils"; * Each test will call different functions on SlidingSync which may depend on state from previous tests. */ describe("SlidingSync", () => { - let client: MatrixClient = null; - let httpBackend: MockHttpBackend = null; + let client: MatrixClient | undefined; + let httpBackend: MockHttpBackend | undefined; const selfUserId = "@alice:localhost"; const selfAccessToken = "aseukfgwef"; const proxyBaseUrl = "http://localhost:8008"; @@ -46,9 +46,9 @@ describe("SlidingSync", () => { // tear down client/httpBackend globals const teardownClient = () => { - httpBackend.verifyNoOutstandingExpectation(); - client.stopClient(); - return httpBackend.stop(); + httpBackend!.verifyNoOutstandingExpectation(); + client!.stopClient(); + return httpBackend!.stop(); }; describe("start/stop", () => { @@ -57,14 +57,14 @@ describe("SlidingSync", () => { let slidingSync: SlidingSync; it("should start the sync loop upon calling start()", async () => { - slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1); + slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1); const fakeResp = { pos: "a", lists: [], rooms: {}, extensions: {}, }; - httpBackend.when("POST", syncUrl).respond(200, fakeResp); + httpBackend!.when("POST", syncUrl).respond(200, fakeResp); const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => { expect(state).toEqual(SlidingSyncState.RequestFinished); expect(resp).toEqual(fakeResp); @@ -72,13 +72,13 @@ describe("SlidingSync", () => { return true; }); slidingSync.start(); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await p; }); it("should stop the sync loop upon calling stop()", () => { slidingSync.stop(); - httpBackend.verifyNoOutstandingExpectation(); + httpBackend!.verifyNoOutstandingExpectation(); }); }); @@ -103,9 +103,9 @@ describe("SlidingSync", () => { it("should be able to subscribe to a room", async () => { // add the subscription - slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1); + slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1); slidingSync.modifyRoomSubscriptions(new Set([roomId])); - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.log("room sub", body); expect(body.room_subscriptions).toBeTruthy(); @@ -125,7 +125,7 @@ describe("SlidingSync", () => { return true; }); slidingSync.start(); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await p; }); @@ -137,7 +137,7 @@ describe("SlidingSync", () => { ["m.room.member", "*"], ], }; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.log("adjusted sub", body); expect(body.room_subscriptions).toBeTruthy(); @@ -158,7 +158,7 @@ describe("SlidingSync", () => { }); slidingSync.modifyRoomSubscriptionInfo(newSubInfo); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await p; // need to set what the new subscription info is for subsequent tests roomSubInfo = newSubInfo; @@ -179,7 +179,7 @@ describe("SlidingSync", () => { required_state: [], timeline: [], }; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.log("new subs", body); expect(body.room_subscriptions).toBeTruthy(); @@ -204,12 +204,12 @@ describe("SlidingSync", () => { const subs = slidingSync.getRoomSubscriptions(); subs.add(anotherRoomID); slidingSync.modifyRoomSubscriptions(subs); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await p; }); it("should be able to unsubscribe from a room", async () => { - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.log("unsub request", body); expect(body.room_subscriptions).toBeFalsy(); @@ -226,7 +226,7 @@ describe("SlidingSync", () => { // remove the subscription for the first room slidingSync.modifyRoomSubscriptions(new Set([anotherRoomID])); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await p; slidingSync.stop(); @@ -273,8 +273,8 @@ describe("SlidingSync", () => { is_dm: true, }, }; - slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client, 1); - httpBackend.when("POST", syncUrl).check(function(req) { + slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client!, 1); + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.log("list", body); expect(body.lists).toBeTruthy(); @@ -301,7 +301,7 @@ describe("SlidingSync", () => { return state === SlidingSyncState.Complete; }); slidingSync.start(); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await responseProcessed; expect(listenerData[roomA]).toEqual(rooms[roomA]); @@ -327,7 +327,7 @@ describe("SlidingSync", () => { it("should be possible to adjust list ranges", async () => { // modify the list ranges - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.log("next ranges", body.lists[0].ranges); expect(body.lists).toBeTruthy(); @@ -351,7 +351,7 @@ describe("SlidingSync", () => { return state === SlidingSyncState.RequestFinished; }); slidingSync.setListRanges(0, newRanges); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await responseProcessed; }); @@ -364,7 +364,7 @@ describe("SlidingSync", () => { "is_dm": true, }, }; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.log("extra list", body); expect(body.lists).toBeTruthy(); @@ -403,13 +403,13 @@ describe("SlidingSync", () => { return state === SlidingSyncState.Complete; }); slidingSync.setList(1, extraListReq); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await responseProcessed; }); it("should be possible to get list DELETE/INSERTs", async () => { // move C (2) to A (0) - httpBackend.when("POST", syncUrl).respond(200, { + httpBackend!.when("POST", syncUrl).respond(200, { pos: "e", lists: [{ count: 500, @@ -440,12 +440,12 @@ describe("SlidingSync", () => { let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { return state === SlidingSyncState.Complete; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await responseProcessed; await listPromise; // move C (0) back to A (2) - httpBackend.when("POST", syncUrl).respond(200, { + httpBackend!.when("POST", syncUrl).respond(200, { pos: "f", lists: [{ count: 500, @@ -476,13 +476,13 @@ describe("SlidingSync", () => { responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { return state === SlidingSyncState.Complete; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await responseProcessed; await listPromise; }); it("should ignore invalid list indexes", async () => { - httpBackend.when("POST", syncUrl).respond(200, { + httpBackend!.when("POST", syncUrl).respond(200, { pos: "e", lists: [{ count: 500, @@ -509,13 +509,13 @@ describe("SlidingSync", () => { const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { return state === SlidingSyncState.Complete; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await responseProcessed; await listPromise; }); it("should be possible to update a list", async () => { - httpBackend.when("POST", syncUrl).respond(200, { + httpBackend!.when("POST", syncUrl).respond(200, { pos: "g", lists: [{ count: 42, @@ -555,7 +555,7 @@ describe("SlidingSync", () => { const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { return state === SlidingSyncState.Complete; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await responseProcessed; await listPromise; }); @@ -567,7 +567,7 @@ describe("SlidingSync", () => { 1: roomC, }; expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId); - httpBackend.when("POST", syncUrl).respond(200, { + httpBackend!.when("POST", syncUrl).respond(200, { pos: "f", // currently the list is [B,C] so we will insert D then immediately delete it lists: [{ @@ -598,7 +598,7 @@ describe("SlidingSync", () => { const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { return state === SlidingSyncState.Complete; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await responseProcessed; await listPromise; }); @@ -608,7 +608,7 @@ describe("SlidingSync", () => { 0: roomB, 1: roomC, }); - httpBackend.when("POST", syncUrl).respond(200, { + httpBackend!.when("POST", syncUrl).respond(200, { pos: "g", lists: [{ count: 499, @@ -634,7 +634,7 @@ describe("SlidingSync", () => { const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { return state === SlidingSyncState.Complete; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await responseProcessed; await listPromise; }); @@ -643,7 +643,7 @@ describe("SlidingSync", () => { expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({ 0: roomC, }); - httpBackend.when("POST", syncUrl).respond(200, { + httpBackend!.when("POST", syncUrl).respond(200, { pos: "h", lists: [{ count: 500, @@ -670,11 +670,11 @@ describe("SlidingSync", () => { let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { return state === SlidingSyncState.Complete; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await responseProcessed; await listPromise; - httpBackend.when("POST", syncUrl).respond(200, { + httpBackend!.when("POST", syncUrl).respond(200, { pos: "h", lists: [{ count: 501, @@ -702,7 +702,7 @@ describe("SlidingSync", () => { responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { return state === SlidingSyncState.Complete; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await responseProcessed; await listPromise; slidingSync.stop(); @@ -725,11 +725,11 @@ describe("SlidingSync", () => { ], }; // add the subscription - slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1); + slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1); // modification before SlidingSync.start() const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId])); let txnId; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); expect(body.room_subscriptions).toBeTruthy(); @@ -752,7 +752,7 @@ describe("SlidingSync", () => { }; }); slidingSync.start(); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await subscribePromise; }); it("should resolve setList during a connection", async () => { @@ -761,7 +761,7 @@ describe("SlidingSync", () => { }; const promise = slidingSync.setList(0, newList); let txnId; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); expect(body.room_subscriptions).toBeFalsy(); @@ -776,14 +776,14 @@ describe("SlidingSync", () => { extensions: {}, }; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await promise; expect(txnId).toBeDefined(); }); it("should resolve setListRanges during a connection", async () => { const promise = slidingSync.setListRanges(0, [[20, 40]]); let txnId; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); expect(body.room_subscriptions).toBeFalsy(); @@ -800,7 +800,7 @@ describe("SlidingSync", () => { extensions: {}, }; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await promise; expect(txnId).toBeDefined(); }); @@ -809,7 +809,7 @@ describe("SlidingSync", () => { timeline_limit: 99, }); let txnId; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); expect(body.room_subscriptions).toBeTruthy(); @@ -825,22 +825,22 @@ describe("SlidingSync", () => { extensions: {}, }; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await promise; expect(txnId).toBeDefined(); }); it("should reject earlier pending promises if a later transaction is acknowledged", async () => { // i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected. - const gotTxnIds = []; + const gotTxnIds: any[] = []; const pushTxn = function(req) { gotTxnIds.push(req.data.txn_id); }; const failPromise = slidingSync.setListRanges(0, [[20, 40]]); - httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id - await httpBackend.flushAllExpected(); + httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id + await httpBackend!.flushAllExpected(); const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]); - httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id - await httpBackend.flushAllExpected(); + httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id + await httpBackend!.flushAllExpected(); // attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection // which is a fail. @@ -849,7 +849,7 @@ describe("SlidingSync", () => { const okPromise = slidingSync.setListRanges(0, [[0, 20]]); let txnId; - httpBackend.when("POST", syncUrl).check((req) => { + httpBackend!.when("POST", syncUrl).check((req) => { txnId = req.data.txn_id; }).respond(200, () => { // include the txn_id, earlier requests should now be reject()ed. @@ -858,23 +858,23 @@ describe("SlidingSync", () => { txn_id: txnId, }; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await okPromise; expect(txnId).toBeDefined(); }); it("should not reject later pending promises if an earlier transaction is acknowledged", async () => { // i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should. - const gotTxnIds = []; + const gotTxnIds: any[] = []; const pushTxn = function(req) { - gotTxnIds.push(req.data.txn_id); + gotTxnIds.push(req.data?.txn_id); }; const A = slidingSync.setListRanges(0, [[20, 40]]); - httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" }); - await httpBackend.flushAllExpected(); + httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" }); + await httpBackend!.flushAllExpected(); const B = slidingSync.setListRanges(0, [[60, 70]]); - httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id - await httpBackend.flushAllExpected(); + httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id + await httpBackend!.flushAllExpected(); // attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection // which is a fail. @@ -885,14 +885,14 @@ describe("SlidingSync", () => { C.finally(() => { pendingC = false; }); - httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, () => { + httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, () => { // include the txn_id for B, so C's promise is outstanding return { pos: "C", txn_id: gotTxnIds[1], }; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); // A is rejected, see above expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved expect(pendingC).toBe(true); // C is pending still @@ -904,7 +904,7 @@ describe("SlidingSync", () => { pending = false; }); let txnId; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); expect(body.room_subscriptions).toBeFalsy(); @@ -921,7 +921,7 @@ describe("SlidingSync", () => { extensions: {}, }; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); expect(txnId).toBeDefined(); expect(pending).toBe(true); slidingSync.stop(); @@ -963,10 +963,10 @@ describe("SlidingSync", () => { }; it("should be able to register an extension", async () => { - slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1); + slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1); slidingSync.registerExtension(extPre); - const callbackOrder = []; + const callbackOrder: string[] = []; let extensionOnResponseCalled = false; onPreExtensionRequest = () => { return extReq; @@ -977,7 +977,7 @@ describe("SlidingSync", () => { expect(resp).toEqual(extResp); }; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.log("ext req", body); expect(body.extensions).toBeTruthy(); @@ -998,7 +998,7 @@ describe("SlidingSync", () => { } }); slidingSync.start(); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await p; expect(extensionOnResponseCalled).toBe(true); expect(callbackOrder).toEqual(["onPreExtensionResponse", "Lifecycle"]); @@ -1012,7 +1012,7 @@ describe("SlidingSync", () => { onPreExtensionResponse = (resp) => { responseCalled = true; }; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.log("ext req nothing", body); expect(body.extensions).toBeTruthy(); @@ -1030,7 +1030,7 @@ describe("SlidingSync", () => { const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => { return state === SlidingSyncState.Complete; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await p; expect(responseCalled).toBe(false); }); @@ -1041,13 +1041,13 @@ describe("SlidingSync", () => { return extReq; }; let responseCalled = false; - const callbackOrder = []; + const callbackOrder: string[] = []; onPostExtensionResponse = (resp) => { expect(resp).toEqual(extResp); responseCalled = true; callbackOrder.push("onPostExtensionResponse"); }; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.log("ext req after start", body); expect(body.extensions).toBeTruthy(); @@ -1071,7 +1071,7 @@ describe("SlidingSync", () => { return true; } }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); await p; expect(responseCalled).toBe(true); expect(callbackOrder).toEqual(["Lifecycle", "onPostExtensionResponse"]); @@ -1079,7 +1079,7 @@ describe("SlidingSync", () => { }); it("is not possible to register the same extension name twice", async () => { - slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1); + slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1); slidingSync.registerExtension(extPre); expect(() => { slidingSync.registerExtension(extPre); }).toThrow(); }); @@ -1106,7 +1106,7 @@ function listenUntil( callback: (...args: any[]) => T, timeoutMs = 500, ): Promise { - const trace = new Error().stack.split(`\n`)[2]; + const trace = new Error().stack?.split(`\n`)[2]; return Promise.race([new Promise((resolve, reject) => { const wrapper = (...args) => { try { From 6e5326f9c8a8b25b657658bfac9c8a0fd85f6aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Oct 2022 16:40:30 +0200 Subject: [PATCH 30/48] Add custom notification handling for MSC3401 call events (#2720) --- spec/test-utils/test-utils.ts | 2 + spec/unit/pushprocessor.spec.ts | 102 +++++++++++++++++++++++++++++++- src/pushprocessor.ts | 42 ++++++++++++- 3 files changed, 142 insertions(+), 4 deletions(-) diff --git a/spec/test-utils/test-utils.ts b/spec/test-utils/test-utils.ts index 0187b12f5..288e0355b 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -74,6 +74,7 @@ interface IEventOpts { sender?: string; skey?: string; content: IContent; + prev_content?: IContent; user?: string; unsigned?: IUnsigned; redacts?: string; @@ -103,6 +104,7 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC room_id: opts.room, sender: opts.sender || opts.user, // opts.user for backwards-compat content: opts.content, + prev_content: opts.prev_content, unsigned: opts.unsigned || {}, event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(), txn_id: "~" + Math.random(), diff --git a/spec/unit/pushprocessor.spec.ts b/spec/unit/pushprocessor.spec.ts index 3bbdd5233..db4d2a417 100644 --- a/spec/unit/pushprocessor.spec.ts +++ b/spec/unit/pushprocessor.spec.ts @@ -1,6 +1,6 @@ import * as utils from "../test-utils/test-utils"; -import { PushProcessor } from "../../src/pushprocessor"; -import { EventType, MatrixClient, MatrixEvent } from "../../src"; +import { IActionsObject, PushProcessor } from "../../src/pushprocessor"; +import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src"; describe('NotificationService', function() { const testUserId = "@ali:matrix.org"; @@ -336,4 +336,102 @@ describe('NotificationService', function() { enabled: true, }, testEvent)).toBe(true); }); + + describe("performCustomEventHandling()", () => { + const getActionsForEvent = (prevContent: IContent, content: IContent): IActionsObject => { + testEvent = utils.mkEvent({ + type: "org.matrix.msc3401.call", + room: testRoomId, + user: "@alice:foo", + skey: "state_key", + event: true, + content: content, + prev_content: prevContent, + }); + + return pushProcessor.actionsForEvent(testEvent); + }; + + const assertDoesNotify = (actions: IActionsObject): void => { + expect(actions.notify).toBeTruthy(); + expect(actions.tweaks.sound).toBeTruthy(); + expect(actions.tweaks.highlight).toBeFalsy(); + }; + + const assertDoesNotNotify = (actions: IActionsObject): void => { + expect(actions.notify).toBeFalsy(); + expect(actions.tweaks.sound).toBeFalsy(); + expect(actions.tweaks.highlight).toBeFalsy(); + }; + + it.each( + ["m.ring", "m.prompt"], + )("should notify when new group call event appears with %s intent", (intent: string) => { + assertDoesNotify(getActionsForEvent({}, { + "m.intent": intent, + "m.type": "m.voice", + "m.name": "Call", + })); + }); + + it("should notify when a call is un-terminated", () => { + assertDoesNotify(getActionsForEvent({ + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + "m.terminated": "All users left", + }, { + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + })); + }); + + it("should not notify when call is terminated", () => { + assertDoesNotNotify(getActionsForEvent({ + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + }, { + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + "m.terminated": "All users left", + })); + }); + + it("should ignore with m.room intent", () => { + assertDoesNotNotify(getActionsForEvent({}, { + "m.intent": "m.room", + "m.type": "m.voice", + "m.name": "Call", + })); + }); + + describe("ignoring non-relevant state changes", () => { + it("should ignore intent changes", () => { + assertDoesNotNotify(getActionsForEvent({ + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + }, { + "m.intent": "m.ring", + "m.type": "m.video", + "m.name": "Call", + })); + }); + + it("should ignore name changes", () => { + assertDoesNotNotify(getActionsForEvent({ + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + }, { + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "New call", + })); + }); + }); + }); }); diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts index 4e736c3a1..1caa78953 100644 --- a/src/pushprocessor.ts +++ b/src/pushprocessor.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils"; +import { deepCompare, escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils"; import { logger } from './logger'; import { MatrixClient } from "./client"; import { MatrixEvent } from "./models/event"; @@ -91,6 +91,20 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [ ], actions: [], }, + { + // For homeservers which don't support MSC3401 yet + rule_id: ".org.matrix.msc3401.rule.room.call", + default: true, + enabled: true, + conditions: [ + { + kind: ConditionKind.EventMatch, + key: "type", + pattern: "org.matrix.msc3401.call", + }, + ], + actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Sound, value: "default" }], + }, ]; export interface IActionsObject { @@ -424,7 +438,7 @@ export class PushProcessor { return {} as IActionsObject; } - const actionObj = PushProcessor.actionListToActionsObject(rule.actions); + let actionObj = PushProcessor.actionListToActionsObject(rule.actions); // Some actions are implicit in some situations: we add those here if (actionObj.tweaks.highlight === undefined) { @@ -433,6 +447,30 @@ export class PushProcessor { actionObj.tweaks.highlight = (rule.kind == PushRuleKind.ContentSpecific); } + actionObj = this.performCustomEventHandling(ev, actionObj); + + return actionObj; + } + + /** + * Some events require custom event handling e.g. due to missing server support + */ + private performCustomEventHandling(ev: MatrixEvent, actionObj: IActionsObject): IActionsObject { + switch (ev.getType()) { + case "m.call": + case "org.matrix.msc3401.call": + // Since servers don't support properly sending push notification + // about MSC3401 call events, we do the handling ourselves + if ( + ev.getContent()["m.intent"] === "m.room" + || ("m.terminated" in ev.getContent()) + || !("m.terminated" in ev.getPrevContent()) && !deepCompare(ev.getPrevContent(), {}) + ) { + actionObj.notify = false; + actionObj.tweaks = {}; + } + } + return actionObj; } From 029280b9d9c38564ee3821089c0a4afa2337a8b6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 7 Oct 2022 00:41:57 -0400 Subject: [PATCH 31/48] Update hidden characters regex (#2738) --- src/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 818da7f64..e4b8b466e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -334,8 +334,9 @@ export function normalize(str: string): string { // Arabic Letter RTL mark U+061C // Combining characters U+0300 - U+036F // Zero width no-break space (BOM) U+FEFF +// Blank/invisible characters (U2800, U2062-U2063) // eslint-disable-next-line no-misleading-character-class -const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036F\uFEFF\u061C\s]/g; +const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036F\uFEFF\u061C\u2800\u2062-\u2063\s]/g; export function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); From 62007ec67359f34d58ddb520a069596b8b0f7857 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 7 Oct 2022 11:38:53 +0100 Subject: [PATCH 32/48] Fix sync init when thread unread notif is not supported (#2739) Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- spec/unit/feature.spec.ts | 62 +++++++++++++++++++++++++++++++++++++++ spec/unit/filter.spec.ts | 3 +- src/@types/sync.ts | 4 +-- src/client.ts | 12 +++++++- src/feature.ts | 62 +++++++++++++++++++++++++++++++++++++++ src/filter-component.ts | 2 +- src/filter.ts | 16 ++++++++-- src/store/memory.ts | 2 +- src/sync.ts | 11 +++---- 9 files changed, 160 insertions(+), 14 deletions(-) create mode 100644 spec/unit/feature.spec.ts create mode 100644 src/feature.ts diff --git a/spec/unit/feature.spec.ts b/spec/unit/feature.spec.ts new file mode 100644 index 000000000..97420947d --- /dev/null +++ b/spec/unit/feature.spec.ts @@ -0,0 +1,62 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { buildFeatureSupportMap, Feature, ServerSupport } from "../../src/feature"; + +describe("Feature detection", () => { + it("checks the matrix version", async () => { + const support = await buildFeatureSupportMap({ + versions: ["v1.3"], + unstable_features: {}, + }); + + expect(support.get(Feature.Thread)).toBe(ServerSupport.Stable); + expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported); + }); + + it("checks the matrix msc number", async () => { + const support = await buildFeatureSupportMap({ + versions: ["v1.2"], + unstable_features: { + "org.matrix.msc3771": true, + "org.matrix.msc3773": true, + }, + }); + expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unstable); + }); + + it("requires two MSCs to pass", async () => { + const support = await buildFeatureSupportMap({ + versions: ["v1.2"], + unstable_features: { + "org.matrix.msc3771": false, + "org.matrix.msc3773": true, + }, + }); + expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported); + }); + + it("requires two MSCs OR matrix versions to pass", async () => { + const support = await buildFeatureSupportMap({ + versions: ["v1.4"], + unstable_features: { + "org.matrix.msc3771": false, + "org.matrix.msc3773": true, + }, + }); + expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Stable); + }); +}); diff --git a/spec/unit/filter.spec.ts b/spec/unit/filter.spec.ts index 925915729..e246ec1a2 100644 --- a/spec/unit/filter.spec.ts +++ b/spec/unit/filter.spec.ts @@ -1,3 +1,4 @@ +import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync"; import { Filter, IFilterDefinition } from "../../src/filter"; describe("Filter", function() { @@ -50,7 +51,7 @@ describe("Filter", function() { expect(filter.getDefinition()).toEqual({ room: { timeline: { - unread_thread_notifications: true, + [UNREAD_THREAD_NOTIFICATIONS.name]: true, }, }, }); diff --git a/src/@types/sync.ts b/src/@types/sync.ts index f25bbf2e4..036c542ba 100644 --- a/src/@types/sync.ts +++ b/src/@types/sync.ts @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { UnstableValue } from "matrix-events-sdk/lib/NamespacedValue"; +import { ServerControlledNamespacedValue } from "../NamespacedValue"; /** * https://github.com/matrix-org/matrix-doc/pull/3773 * * @experimental */ -export const UNREAD_THREAD_NOTIFICATIONS = new UnstableValue( +export const UNREAD_THREAD_NOTIFICATIONS = new ServerControlledNamespacedValue( "unread_thread_notifications", "org.matrix.msc3773.unread_thread_notifications"); diff --git a/src/client.ts b/src/client.ts index 80296c472..2858cab7d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -204,6 +204,8 @@ import { MAIN_ROOM_TIMELINE } from "./models/read-receipt"; 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"; export type Store = IStore; @@ -528,7 +530,7 @@ export interface ITurnServer { credential: string; } -interface IServerVersions { +export interface IServerVersions { versions: string[]; unstable_features: Record; } @@ -967,6 +969,8 @@ export class MatrixClient extends TypedEventEmitter; protected canResetTimelineCallback: ResetTimelineCallback; + public canSupport = new Map(); + // The pushprocessor caches useful things, so keep one and re-use it protected pushProcessor = new PushProcessor(this); @@ -1197,6 +1201,12 @@ export class MatrixClient extends TypedEventEmitter = { + [Feature.Thread]: { + unstablePrefixes: ["org.matrix.msc3440"], + matrixVersion: "v1.3", + }, + [Feature.ThreadUnreadNotifications]: { + unstablePrefixes: ["org.matrix.msc3771", "org.matrix.msc3773"], + matrixVersion: "v1.4", + }, +}; + +export async function buildFeatureSupportMap(versions: IServerVersions): Promise> { + const supportMap = new Map(); + for (const [feature, supportCondition] of Object.entries(featureSupportResolver)) { + const supportMatrixVersion = versions.versions?.includes(supportCondition.matrixVersion || "") ?? false; + const supportUnstablePrefixes = supportCondition.unstablePrefixes?.every(unstablePrefix => { + return versions.unstable_features?.[unstablePrefix] === true; + }) ?? false; + if (supportMatrixVersion) { + supportMap.set(feature as Feature, ServerSupport.Stable); + } else if (supportUnstablePrefixes) { + supportMap.set(feature as Feature, ServerSupport.Unstable); + } else { + supportMap.set(feature as Feature, ServerSupport.Unsupported); + } + } + return supportMap; +} diff --git a/src/filter-component.ts b/src/filter-component.ts index 8cfbea667..5e38238c6 100644 --- a/src/filter-component.ts +++ b/src/filter-component.ts @@ -73,7 +73,7 @@ export interface IFilterComponent { * @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true } */ export class FilterComponent { - constructor(private filterJson: IFilterComponent, public readonly userId?: string) {} + constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {} /** * Checks with the filter component matches the given event diff --git a/src/filter.ts b/src/filter.ts index 0cf2d1c99..14565c26f 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -22,6 +22,7 @@ import { EventType, RelationType, } from "./@types/event"; +import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; import { FilterComponent, IFilterComponent } from "./filter-component"; import { MatrixEvent } from "./models/event"; @@ -99,7 +100,7 @@ export class Filter { * @param {Object} jsonObj * @return {Filter} */ - public static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter { + public static fromJson(userId: string | undefined | null, filterId: string, jsonObj: IFilterDefinition): Filter { const filter = new Filter(userId, filterId); filter.setDefinition(jsonObj); return filter; @@ -109,7 +110,7 @@ export class Filter { private roomFilter: FilterComponent; private roomTimelineFilter: FilterComponent; - constructor(public readonly userId: string, public filterId?: string) {} + constructor(public readonly userId: string | undefined | null, public filterId?: string) {} /** * Get the ID of this filter on your homeserver (if known) @@ -227,7 +228,16 @@ export class Filter { * @param {boolean} enabled */ public setUnreadThreadNotifications(enabled: boolean): void { - setProp(this.definition, "room.timeline.unread_thread_notifications", !!enabled); + this.definition = { + ...this.definition, + room: { + ...this.definition?.room, + timeline: { + ...this.definition?.room?.timeline, + [UNREAD_THREAD_NOTIFICATIONS.name]: !!enabled, + }, + }, + }; } setLazyLoadMembers(enabled: boolean): void { diff --git a/src/store/memory.ts b/src/store/memory.ts index 0ed43a5b5..b44f24ca4 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -227,7 +227,7 @@ export class MemoryStore implements IStore { * @param {Filter} filter */ public storeFilter(filter: Filter): void { - if (!filter) { + if (!filter?.userId) { return; } if (!this.filters[filter.userId]) { diff --git a/src/sync.ts b/src/sync.ts index 0026831d5..cee5e7f09 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -59,6 +59,7 @@ import { BeaconEvent } from "./models/beacon"; import { IEventsResponse } from "./@types/requests"; import { IAbortablePromise } from "./@types/partials"; import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; +import { Feature, ServerSupport } from "./feature"; const DEBUG = true; @@ -562,7 +563,11 @@ export class SyncApi { }; private buildDefaultFilter = () => { - return new Filter(this.client.credentials.userId); + const filter = new Filter(this.client.credentials.userId); + if (this.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) { + filter.setUnreadThreadNotifications(true); + } + return filter; }; private checkLazyLoadStatus = async () => { @@ -706,10 +711,6 @@ export class SyncApi { const initialFilter = this.buildDefaultFilter(); initialFilter.setDefinition(filter.getDefinition()); initialFilter.setTimelineLimit(this.opts.initialSyncLimit); - const supportsThreadNotifications = - await this.client.doesServerSupportUnstableFeature("org.matrix.msc3773") - || await this.client.isVersionSupported("v1.4"); - initialFilter.setUnreadThreadNotifications(supportsThreadNotifications); // Use an inline filter, no point uploading it for a single usage firstSyncFilter = JSON.stringify(initialFilter.getDefinition()); } From e37aab29672256218d30876e96dd17fedc0506f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 7 Oct 2022 16:40:40 +0200 Subject: [PATCH 33/48] Fix `power_level_content_override` type (#2741) --- src/@types/requests.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/@types/requests.ts b/src/@types/requests.ts index 9d0472cee..4246cbab6 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -22,7 +22,7 @@ import { IRoomEventFilter } from "../filter"; import { Direction } from "../models/event-timeline"; import { PushRuleAction } from "./PushRules"; import { IRoomEvent } from "../sync-accumulator"; -import { RoomType } from "./event"; +import { EventType, RoomType } from "./event"; // allow camelcase as these are things that go onto the wire /* eslint-disable camelcase */ @@ -98,7 +98,18 @@ export interface ICreateRoomOpts { name?: string; topic?: string; preset?: Preset; - power_level_content_override?: object; + power_level_content_override?: { + ban?: number; + events?: Record; + events_default?: number; + invite?: number; + kick?: number; + notifications?: Record; + redact?: number; + state_default?: number; + users?: Record; + users_default?: number; + }; creation_content?: object; initial_state?: ICreateRoomStateEvent[]; invite?: string[]; From fe2c35092e8f81ea77415a866ebeeedd4a061482 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 10 Oct 2022 13:34:33 +0100 Subject: [PATCH 34/48] Upgrade to Olm 3.2.13 which has been repackaged to support Node 18 (#2744) --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f845212fe..eb258d70c 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "@babel/preset-env": "^7.12.11", "@babel/preset-typescript": "^7.12.7", "@babel/register": "^7.12.10", - "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz", "@types/bs58": "^4.0.1", "@types/content-type": "^1.1.5", "@types/jest": "^29.0.0", diff --git a/yarn.lock b/yarn.lock index 3f7f511b7..bf9525555 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1367,9 +1367,9 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz": - version "3.2.12" - resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz#0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9" +"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz": + version "3.2.13" + resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz#0109fde93bcc61def851f79826c9384c073b5175" "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" From 6ba35e9fbc122488446a6e2c5e6450afe0767d57 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 Oct 2022 13:41:58 +0100 Subject: [PATCH 35/48] Prepare changelog for v20.1.0 --- CHANGELOG.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cb01fff2..ce41de1aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,4 @@ -Changes in [20.1.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0-rc.2) (2022-10-05) -============================================================================================================ - -## 🐛 Bug Fixes - * Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374. - -Changes in [20.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0-rc.1) (2022-10-04) +Changes in [20.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0) (2022-10-11) ============================================================================================================ ## ✨ Features @@ -15,6 +9,7 @@ Changes in [20.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta * Read receipts for threads ([\#2635](https://github.com/matrix-org/matrix-js-sdk/pull/2635)). ## 🐛 Bug Fixes + * Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374. * Unexpected ignored self key request when it's not shared history ([\#2724](https://github.com/matrix-org/matrix-js-sdk/pull/2724)). Contributed by @mcalinghee. * Fix IDB initial migration handling causing spurious lazy loading upgrade loops ([\#2718](https://github.com/matrix-org/matrix-js-sdk/pull/2718)). Fixes vector-im/element-web#23377. * Fix backpagination at end logic being spec non-conforming ([\#2680](https://github.com/matrix-org/matrix-js-sdk/pull/2680)). Fixes vector-im/element-web#22784. From aa5a34948aac9795c4fe4d3c8f6c6ead04b58b61 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 Oct 2022 13:41:59 +0100 Subject: [PATCH 36/48] v20.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab720d111..90a606bee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "20.1.0-rc.2", + "version": "20.1.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=12.9.0" From 8eed354e17001cd25e3cafe81f74dab499a9882e Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 11 Oct 2022 13:43:31 +0100 Subject: [PATCH 37/48] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 45ea6fb34..fda529c91 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "keywords": [ "matrix-org" ], - "main": "./lib/index.js", + "main": "./src/index.ts", "browser": "./lib/browser-index.js", "matrix_src_main": "./src/index.ts", "matrix_src_browser": "./src/browser-index.js", @@ -127,6 +127,5 @@ "jestSonar": { "reportPath": "coverage", "sonar56x": true - }, - "typings": "./lib/index.d.ts" + } } From 913660c818c3ded5b763c54b43ef745f0150517e Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Wed, 12 Oct 2022 16:56:50 +0200 Subject: [PATCH 38/48] Correct the dir parameter of MSC3715 (#2745) --- spec/integ/matrix-client-relations.spec.ts | 127 +++++++++++++++++++++ src/@types/requests.ts | 2 +- src/client.ts | 6 +- src/models/thread.ts | 4 +- 4 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 spec/integ/matrix-client-relations.spec.ts diff --git a/spec/integ/matrix-client-relations.spec.ts b/spec/integ/matrix-client-relations.spec.ts new file mode 100644 index 000000000..3a8a99fbf --- /dev/null +++ b/spec/integ/matrix-client-relations.spec.ts @@ -0,0 +1,127 @@ +/* +Copyright 2022 Dominik Henneke +Copyright 2022 Nordeck IT + Consulting GmbH. + +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 HttpBackend from "matrix-mock-request"; + +import { Direction, MatrixClient, MatrixScheduler } from "../../src/matrix"; +import { TestClient } from "../TestClient"; + +describe("MatrixClient relations", () => { + const userId = "@alice:localhost"; + const accessToken = "aseukfgwef"; + const roomId = "!room:here"; + let client: MatrixClient | undefined; + let httpBackend: HttpBackend | undefined; + + const setupTests = (): [MatrixClient, HttpBackend] => { + const scheduler = new MatrixScheduler(); + const testClient = new TestClient( + userId, + "DEVICE", + accessToken, + undefined, + { scheduler }, + ); + const httpBackend = testClient.httpBackend; + const client = testClient.client; + + return [client, httpBackend]; + }; + + beforeEach(() => { + [client, httpBackend] = setupTests(); + }); + + afterEach(() => { + httpBackend!.verifyNoOutstandingExpectation(); + return httpBackend!.stop(); + }); + + it("should read related events with the default options", async () => { + const response = client!.relations(roomId, '$event-0', null, null); + + httpBackend! + .when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b") + .respond(200, { chunk: [], next_batch: 'NEXT' }); + + await httpBackend!.flushAllExpected(); + + expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" }); + }); + + it("should read related events with relation type", async () => { + const response = client!.relations(roomId, '$event-0', 'm.reference', null); + + httpBackend! + .when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b") + .respond(200, { chunk: [], next_batch: 'NEXT' }); + + await httpBackend!.flushAllExpected(); + + expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" }); + }); + + it("should read related events with relation type and event type", async () => { + const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message'); + + httpBackend! + .when( + "GET", + "/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b", + ) + .respond(200, { chunk: [], next_batch: 'NEXT' }); + + await httpBackend!.flushAllExpected(); + + expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" }); + }); + + it("should read related events with custom options", async () => { + const response = client!.relations(roomId, '$event-0', null, null, { + dir: Direction.Forward, + from: 'FROM', + limit: 10, + to: 'TO', + }); + + httpBackend! + .when( + "GET", + "/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO", + ) + .respond(200, { chunk: [], next_batch: 'NEXT' }); + + await httpBackend!.flushAllExpected(); + + expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" }); + }); + + it('should use default direction in the fetchRelations endpoint', async () => { + const response = client!.fetchRelations(roomId, '$event-0', null, null); + + httpBackend! + .when( + "GET", + "/rooms/!room%3Ahere/relations/%24event-0?dir=b", + ) + .respond(200, { chunk: [], next_batch: 'NEXT' }); + + await httpBackend!.flushAllExpected(); + + expect(await response).toEqual({ "chunk": [], "next_batch": "NEXT" }); + }); +}); diff --git a/src/@types/requests.ts b/src/@types/requests.ts index 4246cbab6..cb292bf2c 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -160,7 +160,7 @@ export interface IRelationsRequestOpts { from?: string; to?: string; limit?: number; - direction?: Direction; + dir?: Direction; } export interface IRelationsResponse { diff --git a/src/client.ts b/src/client.ts index 2858cab7d..a8857abbb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5399,7 +5399,7 @@ export class MatrixClient extends TypedEventEmitter { const queryString = utils.encodeParams(opts as Record); diff --git a/src/models/thread.ts b/src/models/thread.ts index 60757f9c3..dc433a24f 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -406,7 +406,7 @@ export class Thread extends ReadReceipt { return this.timelineSet.getLiveTimeline(); } - public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, direction: Direction.Backward }): Promise<{ + public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, dir: Direction.Backward }): Promise<{ originalEvent: MatrixEvent; events: MatrixEvent[]; nextBatch?: string | null; @@ -438,7 +438,7 @@ export class Thread extends ReadReceipt { return this.client.decryptEventIfNeeded(event); })); - const prependEvents = (opts.direction ?? Direction.Backward) === Direction.Backward; + const prependEvents = (opts.dir ?? Direction.Backward) === Direction.Backward; this.timelineSet.addEventsToTimeline( events, From 34c5598a3f45f9a42c6a7e06277006183bc49332 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 12 Oct 2022 18:59:04 +0100 Subject: [PATCH 39/48] Modernize http-api - move from `browser-request` to `fetch` (#2719) --- README.md | 6 +- package.json | 12 +- spec/TestClient.ts | 5 +- spec/browserify/sync-browserify.spec.ts | 75 +- spec/integ/matrix-client-methods.spec.ts | 248 +++- spec/integ/matrix-client-opts.spec.ts | 7 +- .../integ/matrix-client-room-timeline.spec.ts | 21 +- spec/integ/matrix-client-syncing.spec.ts | 2 +- spec/integ/sliding-sync-sdk.spec.ts | 4 +- spec/setupTests.ts | 19 + spec/unit/autodiscovery.spec.ts | 84 +- spec/unit/crypto/backup.spec.ts | 64 +- spec/unit/crypto/cross-signing.spec.ts | 2 +- spec/unit/crypto/secrets.spec.ts | 9 +- spec/unit/event-mapper.spec.ts | 2 +- .../http-api/__snapshots__/index.spec.ts.snap | 11 + spec/unit/http-api/fetch.spec.ts | 223 ++++ spec/unit/http-api/index.spec.ts | 228 ++++ spec/unit/http-api/utils.spec.ts | 183 +++ spec/unit/matrix-client.spec.ts | 37 +- spec/unit/models/MSC3089TreeSpace.spec.ts | 6 +- spec/unit/pusher.spec.ts | 4 +- spec/unit/queueToDevice.spec.ts | 4 +- spec/unit/read-receipt.spec.ts | 21 +- spec/unit/utils.spec.ts | 23 +- src/@types/global.d.ts | 2 + src/@types/partials.ts | 5 - src/@types/requests.ts | 11 - src/ToDeviceMessageQueue.ts | 6 +- src/autodiscovery.ts | 104 +- src/browser-index.js | 17 +- src/client.ts | 1039 +++++---------- src/crypto/EncryptionSetup.ts | 10 +- src/crypto/dehydration.ts | 2 - src/http-api.ts | 1140 ----------------- src/http-api/errors.ts | 64 + src/http-api/fetch.ts | 327 +++++ src/http-api/index.ts | 216 ++++ src/http-api/interface.ts | 93 ++ src/http-api/method.ts | 22 + src/http-api/prefix.ts | 53 + src/http-api/utils.ts | 149 +++ src/index.ts | 7 +- src/matrix.ts | 64 +- src/models/MSC3089TreeSpace.ts | 4 +- src/scheduler.ts | 6 +- src/sliding-sync-sdk.ts | 2 +- src/sliding-sync.ts | 23 +- src/store/index.ts | 2 +- src/store/indexeddb-backend.ts | 2 +- src/store/indexeddb-remote-backend.ts | 2 +- src/store/indexeddb.ts | 2 +- src/sync.ts | 28 +- src/utils.ts | 20 +- src/webrtc/call.ts | 10 +- yarn.lock | 339 +---- 56 files changed, 2528 insertions(+), 2543 deletions(-) create mode 100644 spec/setupTests.ts create mode 100644 spec/unit/http-api/__snapshots__/index.spec.ts.snap create mode 100644 spec/unit/http-api/fetch.spec.ts create mode 100644 spec/unit/http-api/index.spec.ts create mode 100644 spec/unit/http-api/utils.spec.ts delete mode 100644 src/http-api.ts create mode 100644 src/http-api/errors.ts create mode 100644 src/http-api/fetch.ts create mode 100644 src/http-api/index.ts create mode 100644 src/http-api/interface.ts create mode 100644 src/http-api/method.ts create mode 100644 src/http-api/prefix.ts create mode 100644 src/http-api/utils.ts diff --git a/README.md b/README.md index 257337d2c..cb3b9f811 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,8 @@ In Node.js ---------- Ensure you have the latest LTS version of Node.js installed. - -This SDK targets Node 12 for compatibility, which translates to ES6. If you're using -a bundler like webpack you'll likely have to transpile dependencies, including this -SDK, to match your target browsers. +This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills. +If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options. Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install) if you do not have it already. diff --git a/package.json b/package.json index fda529c91..710bb281d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "20.1.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { - "node": ">=12.9.0" + "node": ">=16.0.0" }, "scripts": { "prepublishOnly": "yarn build", @@ -55,14 +55,12 @@ "dependencies": { "@babel/runtime": "^7.12.5", "another-json": "^0.2.0", - "browser-request": "^0.3.3", "bs58": "^5.0.0", "content-type": "^1.0.4", "loglevel": "^1.7.1", "matrix-events-sdk": "^0.0.1-beta.7", "p-retry": "4", "qs": "^6.9.6", - "request": "^2.88.2", "unhomoglyph": "^1.0.6" }, "devDependencies": { @@ -81,9 +79,9 @@ "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz", "@types/bs58": "^4.0.1", "@types/content-type": "^1.1.5", + "@types/domexception": "^4.0.0", "@types/jest": "^29.0.0", "@types/node": "16", - "@types/request": "^2.48.5", "@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/parser": "^5.6.0", "allchange": "^1.0.6", @@ -92,6 +90,7 @@ "better-docs": "^2.4.0-beta.9", "browserify": "^17.0.0", "docdash": "^1.2.0", + "domexception": "^4.0.0", "eslint": "8.24.0", "eslint-config-google": "^0.14.0", "eslint-import-resolver-typescript": "^3.5.1", @@ -104,7 +103,7 @@ "jest-mock": "^27.5.1", "jest-sonar-reporter": "^2.0.0", "jsdoc": "^3.6.6", - "matrix-mock-request": "^2.1.2", + "matrix-mock-request": "^2.5.0", "rimraf": "^3.0.2", "terser": "^5.5.1", "tsify": "^5.0.2", @@ -115,6 +114,9 @@ "testMatch": [ "/spec/**/*.spec.{js,ts}" ], + "setupFilesAfterEnv": [ + "/spec/setupTests.ts" + ], "collectCoverageFrom": [ "/src/**/*.{js,ts}" ], diff --git a/spec/TestClient.ts b/spec/TestClient.ts index 0a6c4e0ee..6056884dd 100644 --- a/spec/TestClient.ts +++ b/spec/TestClient.ts @@ -30,7 +30,6 @@ import { MockStorageApi } from "./MockStorageApi"; import { encodeUri } from "../src/utils"; import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration"; import { IKeyBackupSession } from "../src/crypto/keybackup"; -import { IHttpOpts } from "../src/http-api"; import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client'; /** @@ -56,11 +55,11 @@ export class TestClient { this.httpBackend = new MockHttpBackend(); const fullOptions: ICreateClientOpts = { - baseUrl: "http://" + userId + ".test.server", + baseUrl: "http://" + userId?.slice(1).replace(":", ".") + ".test.server", userId: userId, accessToken: accessToken, deviceId: deviceId, - request: this.httpBackend.requestFn as IHttpOpts["request"], + fetchFn: this.httpBackend.fetchFn as typeof global.fetch, ...options, }; if (!fullOptions.cryptoStore) { diff --git a/spec/browserify/sync-browserify.spec.ts b/spec/browserify/sync-browserify.spec.ts index 8a087c807..648a72dc4 100644 --- a/spec/browserify/sync-browserify.spec.ts +++ b/spec/browserify/sync-browserify.spec.ts @@ -14,46 +14,66 @@ See the License for the specific language governing permissions and limitations under the License. */ -// load XmlHttpRequest mock +import HttpBackend from "matrix-mock-request"; + import "./setupTests"; import "../../dist/browser-matrix"; // uses browser-matrix instead of the src -import * as utils from "../test-utils/test-utils"; -import { TestClient } from "../TestClient"; +import type { MatrixClient, ClientEvent } from "../../src"; const USER_ID = "@user:test.server"; const DEVICE_ID = "device_id"; const ACCESS_TOKEN = "access_token"; const ROOM_ID = "!room_id:server.test"; +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace NodeJS { + interface Global { + matrixcs: { + MatrixClient: typeof MatrixClient; + ClientEvent: typeof ClientEvent; + }; + } + } +} + describe("Browserify Test", function() { - let client; - let httpBackend; + let client: MatrixClient; + let httpBackend: HttpBackend; beforeEach(() => { - const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN); - - client = testClient.client; - httpBackend = testClient.httpBackend; + httpBackend = new HttpBackend(); + client = new global.matrixcs.MatrixClient({ + baseUrl: "http://test.server", + userId: USER_ID, + accessToken: ACCESS_TOKEN, + deviceId: DEVICE_ID, + fetchFn: httpBackend.fetchFn as typeof global.fetch, + }); httpBackend.when("GET", "/versions").respond(200, {}); httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); - - client.startClient(); }); afterEach(async () => { client.stopClient(); - httpBackend.stop(); + client.http.abort(); + httpBackend.verifyNoOutstandingRequests(); + httpBackend.verifyNoOutstandingExpectation(); + await httpBackend.stop(); }); - it("Sync", function() { - const event = utils.mkMembership({ - room: ROOM_ID, - mship: "join", - user: "@other_user:server.test", - name: "Displayname", - }); + it("Sync", async () => { + const event = { + type: "m.room.member", + room_id: ROOM_ID, + content: { + membership: "join", + name: "Displayname", + }, + event_id: "$foobar", + }; const syncData = { next_batch: "batch1", @@ -71,11 +91,16 @@ describe("Browserify Test", function() { }; httpBackend.when("GET", "/sync").respond(200, syncData); - return Promise.race([ - httpBackend.flushAllExpected(), - new Promise((_, reject) => { - client.once("sync.unexpectedError", reject); - }), - ]); + httpBackend.when("GET", "/sync").respond(200, syncData); + + const syncPromise = new Promise(r => client.once(global.matrixcs.ClientEvent.Sync, r)); + const unexpectedErrorFn = jest.fn(); + client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn); + + client.startClient(); + + await httpBackend.flushAllExpected(); + await syncPromise; + expect(unexpectedErrorFn).not.toHaveBeenCalled(); }, 20000); // additional timeout as this test can take quite a while }); diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index fdf32d9e9..5bec405cb 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -16,13 +16,12 @@ limitations under the License. import HttpBackend from "matrix-mock-request"; import * as utils from "../test-utils/test-utils"; -import { CRYPTO_ENABLED, MatrixClient, IStoredClientOpts } from "../../src/client"; +import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client"; import { MatrixEvent } from "../../src/models/event"; -import { Filter, MemoryStore, Room } from "../../src/matrix"; +import { Filter, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix"; import { TestClient } from "../TestClient"; import { THREAD_RELATION_TYPE } from "../../src/models/thread"; import { IFilterDefinition } from "../../src/filter"; -import { FileType } from "../../src/http-api"; import { ISearchResults } from "../../src/@types/search"; import { IStore } from "../../src/store"; @@ -65,28 +64,27 @@ describe("MatrixClient", function() { describe("uploadContent", function() { const buf = Buffer.from('hello world'); + const file = buf; + const opts = { + type: "text/plain", + name: "hi.txt", + }; + it("should upload the file", function() { httpBackend!.when( "POST", "/_matrix/media/r0/upload", ).check(function(req) { expect(req.rawData).toEqual(buf); expect(req.queryParams?.filename).toEqual("hi.txt"); - if (!(req.queryParams?.access_token == accessToken || - req.headers["Authorization"] == "Bearer " + accessToken)) { - expect(true).toBe(false); - } + expect(req.headers["Authorization"]).toBe("Bearer " + accessToken); expect(req.headers["Content-Type"]).toEqual("text/plain"); // @ts-ignore private property expect(req.opts.json).toBeFalsy(); // @ts-ignore private property expect(req.opts.timeout).toBe(undefined); - }).respond(200, "content", true); + }).respond(200, '{"content_uri": "content"}', true); - const prom = client!.uploadContent({ - stream: buf, - name: "hi.txt", - type: "text/plain", - } as unknown as FileType); + const prom = client!.uploadContent(file, opts); expect(prom).toBeTruthy(); @@ -96,8 +94,7 @@ describe("MatrixClient", function() { expect(uploads[0].loaded).toEqual(0); const prom2 = prom.then(function(response) { - // for backwards compatibility, we return the raw JSON - expect(response).toEqual("content"); + expect(response.content_uri).toEqual("content"); const uploads = client!.getCurrentUploads(); expect(uploads.length).toEqual(0); @@ -107,28 +104,6 @@ describe("MatrixClient", function() { return prom2; }); - it("should parse the response if rawResponse=false", function() { - httpBackend!.when( - "POST", "/_matrix/media/r0/upload", - ).check(function(req) { - // @ts-ignore private property - expect(req.opts.json).toBeFalsy(); - }).respond(200, { "content_uri": "uri" }); - - const prom = client!.uploadContent({ - stream: buf, - name: "hi.txt", - type: "text/plain", - } as unknown as FileType, { - rawResponse: false, - }).then(function(response) { - expect(response.content_uri).toEqual("uri"); - }); - - httpBackend!.flush(''); - return prom; - }); - it("should parse errors into a MatrixError", function() { httpBackend!.when( "POST", "/_matrix/media/r0/upload", @@ -141,11 +116,7 @@ describe("MatrixClient", function() { "error": "broken", }); - const prom = client!.uploadContent({ - stream: buf, - name: "hi.txt", - type: "text/plain", - } as unknown as FileType).then(function(response) { + const prom = client!.uploadContent(file, opts).then(function(response) { throw Error("request not failed"); }, function(error) { expect(error.httpStatus).toEqual(400); @@ -157,30 +128,18 @@ describe("MatrixClient", function() { return prom; }); - it("should return a promise which can be cancelled", function() { - const prom = client!.uploadContent({ - stream: buf, - name: "hi.txt", - type: "text/plain", - } as unknown as FileType); + it("should return a promise which can be cancelled", async () => { + const prom = client!.uploadContent(file, opts); const uploads = client!.getCurrentUploads(); expect(uploads.length).toEqual(1); expect(uploads[0].promise).toBe(prom); expect(uploads[0].loaded).toEqual(0); - const prom2 = prom.then(function(response) { - throw Error("request not aborted"); - }, function(error) { - expect(error).toEqual("aborted"); - - const uploads = client!.getCurrentUploads(); - expect(uploads.length).toEqual(0); - }); - const r = client!.cancelUpload(prom); expect(r).toBe(true); - return prom2; + await expect(prom).rejects.toThrow("Aborted"); + expect(client.getCurrentUploads()).toHaveLength(0); }); }); @@ -202,6 +161,30 @@ describe("MatrixClient", function() { client!.joinRoom(roomId); httpBackend!.verifyNoOutstandingRequests(); }); + + it("should send request to inviteSignUrl if specified", async () => { + const roomId = "!roomId:server"; + const inviteSignUrl = "https://id.server/sign/this/for/me"; + const viaServers = ["a", "b", "c"]; + const signature = { + sender: "sender", + mxid: "@sender:foo", + token: "token", + signatures: {}, + }; + + httpBackend!.when("POST", inviteSignUrl).respond(200, signature); + httpBackend!.when("POST", "/join/" + encodeURIComponent(roomId)).check(request => { + expect(request.data.third_party_signed).toEqual(signature); + }).respond(200, { room_id: roomId }); + + const prom = client.joinRoom(roomId, { + inviteSignUrl, + viaServers, + }); + await httpBackend!.flushAllExpected(); + expect((await prom).roomId).toBe(roomId); + }); }); describe("getFilter", function() { @@ -676,7 +659,7 @@ describe("MatrixClient", function() { // The vote event has been copied into the thread const eventRefWithThreadId = withThreadId( eventPollResponseReference, eventPollStartThreadRoot.getId()); - expect(eventRefWithThreadId.threadId).toBeTruthy(); + expect(eventRefWithThreadId.threadRootId).toBeTruthy(); expect(threaded).toEqual([ eventPollStartThreadRoot, @@ -1178,15 +1161,150 @@ describe("MatrixClient", function() { expect(await prom).toStrictEqual(response); }); }); + + describe("logout", () => { + it("should abort pending requests when called with stopClient=true", async () => { + httpBackend.when("POST", "/logout").respond(200, {}); + const fn = jest.fn(); + client.http.request(Method.Get, "/test").catch(fn); + client.logout(true); + await httpBackend.flush(undefined); + expect(fn).toHaveBeenCalled(); + }); + }); + + describe("sendHtmlEmote", () => { + it("should send valid html emote", async () => { + httpBackend.when("PUT", "/send").check(req => { + expect(req.data).toStrictEqual({ + "msgtype": "m.emote", + "body": "Body", + "formatted_body": "

Body

", + "format": "org.matrix.custom.html", + "org.matrix.msc1767.message": expect.anything(), + }); + }).respond(200, { event_id: "$foobar" }); + const prom = client.sendHtmlEmote("!room:server", "Body", "

Body

"); + await httpBackend.flush(undefined); + await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" }); + }); + }); + + describe("sendHtmlMessage", () => { + it("should send valid html message", async () => { + httpBackend.when("PUT", "/send").check(req => { + expect(req.data).toStrictEqual({ + "msgtype": "m.text", + "body": "Body", + "formatted_body": "

Body

", + "format": "org.matrix.custom.html", + "org.matrix.msc1767.message": expect.anything(), + }); + }).respond(200, { event_id: "$foobar" }); + const prom = client.sendHtmlMessage("!room:server", "Body", "

Body

"); + await httpBackend.flush(undefined); + await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" }); + }); + }); + + describe("forget", () => { + it("should remove from store by default", async () => { + const room = new Room("!roomId:server", client, userId); + client.store.storeRoom(room); + expect(client.store.getRooms()).toContain(room); + + httpBackend.when("POST", "/forget").respond(200, {}); + await Promise.all([ + client.forget(room.roomId), + httpBackend.flushAllExpected(), + ]); + expect(client.store.getRooms()).not.toContain(room); + }); + }); + + describe("getCapabilities", () => { + it("should cache by default", async () => { + httpBackend!.when("GET", "/capabilities").respond(200, { + capabilities: { + "m.change_password": false, + }, + }); + const prom = httpBackend!.flushAllExpected(); + const capabilities1 = await client!.getCapabilities(); + const capabilities2 = await client!.getCapabilities(); + await prom; + + expect(capabilities1).toStrictEqual(capabilities2); + }); + }); + + describe("getTerms", () => { + it("should return Identity Server terms", async () => { + httpBackend!.when("GET", "/terms").respond(200, { foo: "bar" }); + const prom = client!.getTerms(SERVICE_TYPES.IS, "http://identity.server"); + await httpBackend!.flushAllExpected(); + await expect(prom).resolves.toEqual({ foo: "bar" }); + }); + + it("should return Integrations Manager terms", async () => { + httpBackend!.when("GET", "/terms").respond(200, { foo: "bar" }); + const prom = client!.getTerms(SERVICE_TYPES.IM, "http://im.server"); + await httpBackend!.flushAllExpected(); + await expect(prom).resolves.toEqual({ foo: "bar" }); + }); + }); + + describe("publicRooms", () => { + it("should use GET request if no server or filter is specified", () => { + httpBackend!.when("GET", "/publicRooms").respond(200, {}); + client!.publicRooms({}); + return httpBackend!.flushAllExpected(); + }); + + it("should use GET request if only server is specified", () => { + httpBackend!.when("GET", "/publicRooms").check(request => { + expect(request.queryParams.server).toBe("server1"); + }).respond(200, {}); + client!.publicRooms({ server: "server1" }); + return httpBackend!.flushAllExpected(); + }); + + it("should use POST request if filter is specified", () => { + httpBackend!.when("POST", "/publicRooms").check(request => { + expect(request.data.filter.generic_search_term).toBe("foobar"); + }).respond(200, {}); + client!.publicRooms({ filter: { generic_search_term: "foobar" } }); + return httpBackend!.flushAllExpected(); + }); + }); + + describe("login", () => { + it("should persist values to the client opts", async () => { + const token = "!token&"; + const userId = "@m:t"; + + httpBackend!.when("POST", "/login").respond(200, { + access_token: token, + user_id: userId, + }); + const prom = client!.login("fake.login", {}); + await httpBackend!.flushAllExpected(); + const resp = await prom; + expect(resp.access_token).toBe(token); + expect(resp.user_id).toBe(userId); + expect(client.getUserId()).toBe(userId); + expect(client.http.opts.accessToken).toBe(token); + }); + }); }); -function withThreadId(event, newThreadId) { +function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent { const ret = event.toSnapshot(); ret.setThreadId(newThreadId); return ret; } -const buildEventMessageInThread = (root) => new MatrixEvent({ +const buildEventMessageInThread = (root: MatrixEvent) => new MatrixEvent({ "age": 80098509, "content": { "algorithm": "m.megolm.v1.aes-sha2", @@ -1233,7 +1351,7 @@ const buildEventPollResponseReference = () => new MatrixEvent({ "user_id": "@andybalaam-test1:matrix.org", }); -const buildEventReaction = (event) => new MatrixEvent({ +const buildEventReaction = (event: MatrixEvent) => new MatrixEvent({ "content": { "m.relates_to": { "event_id": event.getId(), @@ -1252,7 +1370,7 @@ const buildEventReaction = (event) => new MatrixEvent({ "room_id": "!STrMRsukXHtqQdSeHa:matrix.org", }); -const buildEventRedaction = (event) => new MatrixEvent({ +const buildEventRedaction = (event: MatrixEvent) => new MatrixEvent({ "content": { }, @@ -1286,7 +1404,7 @@ const buildEventPollStartThreadRoot = () => new MatrixEvent({ "user_id": "@andybalaam-test1:matrix.org", }); -const buildEventReply = (target) => new MatrixEvent({ +const buildEventReply = (target: MatrixEvent) => new MatrixEvent({ "age": 80098509, "content": { "algorithm": "m.megolm.v1.aes-sha2", @@ -1452,7 +1570,7 @@ const buildEventCreate = () => new MatrixEvent({ "user_id": "@andybalaam-test1:matrix.org", }); -function assertObjectContains(obj, expected) { +function assertObjectContains(obj: object, expected: any): void { for (const k in expected) { if (expected.hasOwnProperty(k)) { expect(obj[k]).toEqual(expected[k]); diff --git a/spec/integ/matrix-client-opts.spec.ts b/spec/integ/matrix-client-opts.spec.ts index 714030074..5ea4fba77 100644 --- a/spec/integ/matrix-client-opts.spec.ts +++ b/spec/integ/matrix-client-opts.spec.ts @@ -5,7 +5,6 @@ import { MatrixClient } from "../../src/matrix"; import { MatrixScheduler } from "../../src/scheduler"; import { MemoryStore } from "../../src/store/memory"; import { MatrixError } from "../../src/http-api"; -import { ICreateClientOpts } from "../../src/client"; import { IStore } from "../../src/store"; describe("MatrixClient opts", function() { @@ -69,7 +68,7 @@ describe("MatrixClient opts", function() { let client; beforeEach(function() { client = new MatrixClient({ - request: httpBackend.requestFn as unknown as ICreateClientOpts['request'], + fetchFn: httpBackend.fetchFn as typeof global.fetch, store: undefined, baseUrl: baseUrl, userId: userId, @@ -129,7 +128,7 @@ describe("MatrixClient opts", function() { let client; beforeEach(function() { client = new MatrixClient({ - request: httpBackend.requestFn as unknown as ICreateClientOpts['request'], + fetchFn: httpBackend.fetchFn as typeof global.fetch, store: new MemoryStore() as IStore, baseUrl: baseUrl, userId: userId, @@ -143,7 +142,7 @@ describe("MatrixClient opts", function() { }); it("shouldn't retry sending events", function(done) { - httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({ + httpBackend.when("PUT", "/txn1").respond(500, new MatrixError({ errcode: "M_SOMETHING", error: "Ruh roh", })); diff --git a/spec/integ/matrix-client-room-timeline.spec.ts b/spec/integ/matrix-client-room-timeline.spec.ts index 48ecee32b..42d90d91c 100644 --- a/spec/integ/matrix-client-room-timeline.spec.ts +++ b/spec/integ/matrix-client-room-timeline.spec.ts @@ -18,7 +18,7 @@ import HttpBackend from "matrix-mock-request"; import * as utils from "../test-utils/test-utils"; import { EventStatus } from "../../src/models/event"; -import { ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src"; +import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src"; import { TestClient } from "../TestClient"; describe("MatrixClient room timelines", function() { @@ -802,17 +802,14 @@ describe("MatrixClient room timelines", function() { it('Timeline recovers after `/context` request to generate new timeline fails', async () => { // `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()` // to construct a new timeline from. - httpBackend!.when("GET", contextUrl) - .respond(500, function() { - // The timeline should be cleared at this point in the refresh - expect(room.timeline.length).toEqual(0); - - return { - errcode: 'TEST_FAKE_ERROR', - error: 'We purposely intercepted this /context request to make it fail ' + - 'in order to test whether the refresh timeline code is resilient', - }; - }); + httpBackend!.when("GET", contextUrl).check(() => { + // The timeline should be cleared at this point in the refresh + expect(room.timeline.length).toEqual(0); + }).respond(500, new MatrixError({ + errcode: 'TEST_FAKE_ERROR', + error: 'We purposely intercepted this /context request to make it fail ' + + 'in order to test whether the refresh timeline code is resilient', + })); // Refresh the timeline and expect it to fail const settledFailedRefreshPromises = await Promise.allSettled([ diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index 78795051c..fd8fd7b7d 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -1572,7 +1572,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => { const idbHttpBackend = idbTestClient.httpBackend; const idbClient = idbTestClient.client; idbHttpBackend.when("GET", "/versions").respond(200, {}); - idbHttpBackend.when("GET", "/pushrules").respond(200, {}); + idbHttpBackend.when("GET", "/pushrules/").respond(200, {}); idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" }); await idbClient.initCrypto(); diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 47e6ba8df..3e50064a6 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -23,12 +23,13 @@ import { TestClient } from "../TestClient"; import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator"; import { MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError, - EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, + EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent, } from "../../src"; import { SlidingSyncSdk } from "../../src/sliding-sync-sdk"; import { SyncState } from "../../src/sync"; import { IStoredClientOpts } from "../../src/client"; import { logger } from "../../src/logger"; +import { emitPromise } from "../test-utils/test-utils"; describe("SlidingSyncSdk", () => { let client: MatrixClient | undefined; @@ -530,6 +531,7 @@ describe("SlidingSyncSdk", () => { ], }); await httpBackend!.flush("/profile", 1, 1000); + await emitPromise(client!, RoomMemberEvent.Name); const room = client!.getRoom(roomId)!; expect(room).toBeDefined(); const inviteeMember = room.getMember(invitee)!; diff --git a/spec/setupTests.ts b/spec/setupTests.ts new file mode 100644 index 000000000..bbd70fe3d --- /dev/null +++ b/spec/setupTests.ts @@ -0,0 +1,19 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import DOMException from "domexception"; + +global.DOMException = DOMException; diff --git a/spec/unit/autodiscovery.spec.ts b/spec/unit/autodiscovery.spec.ts index 939f47797..13688c25b 100644 --- a/spec/unit/autodiscovery.spec.ts +++ b/spec/unit/autodiscovery.spec.ts @@ -17,13 +17,12 @@ limitations under the License. import MockHttpBackend from "matrix-mock-request"; -import { request } from "../../src/matrix"; import { AutoDiscovery } from "../../src/autodiscovery"; describe("AutoDiscovery", function() { const getHttpBackend = (): MockHttpBackend => { const httpBackend = new MockHttpBackend(); - request(httpBackend.requestFn); + AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch); return httpBackend; }; @@ -176,8 +175,7 @@ describe("AutoDiscovery", function() { ]); }); - it("should return FAIL_PROMPT when .well-known does not have a base_url for " + - "m.homeserver (empty string)", function() { + it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (empty string)", () => { const httpBackend = getHttpBackend(); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { "m.homeserver": { @@ -205,8 +203,7 @@ describe("AutoDiscovery", function() { ]); }); - it("should return FAIL_PROMPT when .well-known does not have a base_url for " + - "m.homeserver (no property)", function() { + it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (no property)", () => { const httpBackend = getHttpBackend(); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { "m.homeserver": {}, @@ -232,8 +229,7 @@ describe("AutoDiscovery", function() { ]); }); - it("should return FAIL_ERROR when .well-known has an invalid base_url for " + - "m.homeserver (disallowed scheme)", function() { + it("should return FAIL_ERROR when .well-known has an invalid base_url for m.homeserver (disallowed scheme)", () => { const httpBackend = getHttpBackend(); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { "m.homeserver": { @@ -679,4 +675,76 @@ describe("AutoDiscovery", function() { }), ]); }); + + it("should return FAIL_PROMPT for connection errors", () => { + const httpBackend = getHttpBackend(); + httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_PROMPT", + error: AutoDiscovery.ERROR_INVALID, + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_PROMPT for fetch errors", () => { + const httpBackend = getHttpBackend(); + httpBackend.when("GET", "/.well-known/matrix/client").fail(0, new Error("CORS or something")); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_PROMPT", + error: AutoDiscovery.ERROR_INVALID, + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_PROMPT for invalid JSON", () => { + const httpBackend = getHttpBackend(); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "", true); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_PROMPT", + error: AutoDiscovery.ERROR_INVALID, + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); }); diff --git a/spec/unit/crypto/backup.spec.ts b/spec/unit/crypto/backup.spec.ts index acec47fce..ca4c09c53 100644 --- a/spec/unit/crypto/backup.spec.ts +++ b/spec/unit/crypto/backup.spec.ts @@ -30,7 +30,7 @@ import { Crypto } from "../../../src/crypto"; import { resetCrossSigningKeys } from "./crypto-utils"; import { BackupManager } from "../../../src/crypto/backup"; import { StubStore } from "../../../src/store/stub"; -import { IAbortablePromise, MatrixScheduler } from '../../../src'; +import { MatrixScheduler } from '../../../src'; const Olm = global.Olm; @@ -131,7 +131,7 @@ function makeTestClient(cryptoStore) { baseUrl: "https://my.home.server", idBaseUrl: "https://identity.server", accessToken: "my.access.token", - request: jest.fn(), // NOP + fetchFn: jest.fn(), // NOP store: store, scheduler: scheduler, userId: "@alice:bar", @@ -197,7 +197,7 @@ describe("MegolmBackup", function() { // to tick the clock between the first try and the retry. const realSetTimeout = global.setTimeout; jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) { - return realSetTimeout(f, n/100); + return realSetTimeout(f!, n/100); }); }); @@ -298,25 +298,25 @@ describe("MegolmBackup", function() { }); let numCalls = 0; return new Promise((resolve, reject) => { - client.http.authedRequest = function( - callback, method, path, queryParams, data, opts, - ) { + client.http.authedRequest = function( + method, path, queryParams, data, opts, + ): Promise { ++numCalls; expect(numCalls).toBeLessThanOrEqual(1); if (numCalls >= 2) { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({}) as IAbortablePromise; + return Promise.resolve({} as T); } expect(method).toBe("PUT"); expect(path).toBe("/room_keys/keys"); expect(queryParams.version).toBe('1'); - expect(data.rooms[ROOM_ID].sessions).toBeDefined(); - expect(data.rooms[ROOM_ID].sessions).toHaveProperty( + expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); + expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( groupSession.session_id(), ); resolve(); - return Promise.resolve({}) as IAbortablePromise; + return Promise.resolve({} as T); }; client.crypto.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", @@ -381,25 +381,25 @@ describe("MegolmBackup", function() { }); let numCalls = 0; return new Promise((resolve, reject) => { - client.http.authedRequest = function( - callback, method, path, queryParams, data, opts, - ) { + client.http.authedRequest = function( + method, path, queryParams, data, opts, + ): Promise { ++numCalls; expect(numCalls).toBeLessThanOrEqual(1); if (numCalls >= 2) { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({}) as IAbortablePromise; + return Promise.resolve({} as T); } expect(method).toBe("PUT"); expect(path).toBe("/room_keys/keys"); expect(queryParams.version).toBe('1'); - expect(data.rooms[ROOM_ID].sessions).toBeDefined(); - expect(data.rooms[ROOM_ID].sessions).toHaveProperty( + expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); + expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( groupSession.session_id(), ); resolve(); - return Promise.resolve({}) as IAbortablePromise; + return Promise.resolve({} as T); }; client.crypto.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", @@ -439,7 +439,7 @@ describe("MegolmBackup", function() { new Promise((resolve, reject) => { let backupInfo; client.http.authedRequest = function( - callback, method, path, queryParams, data, opts, + method, path, queryParams, data, opts, ) { ++numCalls; expect(numCalls).toBeLessThanOrEqual(2); @@ -449,23 +449,23 @@ describe("MegolmBackup", function() { try { // make sure auth_data is signed by the master key olmlib.pkVerify( - data.auth_data, client.getCrossSigningId(), "@alice:bar", + (data as Record).auth_data, client.getCrossSigningId(), "@alice:bar", ); } catch (e) { reject(e); - return Promise.resolve({}) as IAbortablePromise; + return Promise.resolve({}); } backupInfo = data; - return Promise.resolve({}) as IAbortablePromise; + return Promise.resolve({}); } else if (numCalls === 2) { expect(method).toBe("GET"); expect(path).toBe("/room_keys/version"); resolve(); - return Promise.resolve(backupInfo) as IAbortablePromise; + return Promise.resolve(backupInfo); } else { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many times")); - return Promise.resolve({}) as IAbortablePromise; + return Promise.resolve({}); } }; }), @@ -495,7 +495,7 @@ describe("MegolmBackup", function() { baseUrl: "https://my.home.server", idBaseUrl: "https://identity.server", accessToken: "my.access.token", - request: jest.fn(), // NOP + fetchFn: jest.fn(), // NOP store: store, scheduler: scheduler, userId: "@alice:bar", @@ -542,30 +542,30 @@ describe("MegolmBackup", function() { let numCalls = 0; await new Promise((resolve, reject) => { - client.http.authedRequest = function( - callback, method, path, queryParams, data, opts, - ) { + client.http.authedRequest = function( + method, path, queryParams, data, opts, + ): Promise { ++numCalls; expect(numCalls).toBeLessThanOrEqual(2); if (numCalls >= 3) { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({}) as IAbortablePromise; + return Promise.resolve({} as T); } expect(method).toBe("PUT"); expect(path).toBe("/room_keys/keys"); expect(queryParams.version).toBe('1'); - expect(data.rooms[ROOM_ID].sessions).toBeDefined(); - expect(data.rooms[ROOM_ID].sessions).toHaveProperty( + expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); + expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( groupSession.session_id(), ); if (numCalls > 1) { resolve(); - return Promise.resolve({}) as IAbortablePromise; + return Promise.resolve({} as T); } else { return Promise.reject( new Error("this is an expected failure"), - ) as IAbortablePromise; + ); } }; return client.crypto.backupManager.backupGroupSession( diff --git a/spec/unit/crypto/cross-signing.spec.ts b/spec/unit/crypto/cross-signing.spec.ts index 30c1bf82c..bfa7625cb 100644 --- a/spec/unit/crypto/cross-signing.spec.ts +++ b/spec/unit/crypto/cross-signing.spec.ts @@ -141,7 +141,7 @@ describe("Cross Signing", function() { }; alice.uploadKeySignatures = async () => ({ failures: {} }); alice.setAccountData = async () => ({}); - alice.getAccountDataFromServer = async (): Promise => ({} as T); + alice.getAccountDataFromServer = async (): Promise => ({} as T); const authUploadDeviceSigningKeys = async func => await func({}); // Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts index d6ae8c3a3..7692292ff 100644 --- a/spec/unit/crypto/secrets.spec.ts +++ b/spec/unit/crypto/secrets.spec.ts @@ -109,16 +109,13 @@ describe("Secrets", function() { const secretStorage = alice.crypto.secretStorage; jest.spyOn(alice, 'setAccountData').mockImplementation( - async function(eventType, contents, callback) { + async function(eventType, contents) { alice.store.storeAccountDataEvents([ new MatrixEvent({ type: eventType, content: contents, }), ]); - if (callback) { - callback(undefined, undefined); - } return {}; }); @@ -192,7 +189,7 @@ describe("Secrets", function() { }, }, ); - alice.setAccountData = async function(eventType, contents, callback) { + alice.setAccountData = async function(eventType, contents) { alice.store.storeAccountDataEvents([ new MatrixEvent({ type: eventType, @@ -332,7 +329,7 @@ describe("Secrets", function() { ); bob.uploadDeviceSigningKeys = async () => ({}); bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined); - bob.setAccountData = async function(eventType, contents, callback) { + bob.setAccountData = async function(eventType, contents) { const event = new MatrixEvent({ type: eventType, content: contents, diff --git a/spec/unit/event-mapper.spec.ts b/spec/unit/event-mapper.spec.ts index a444c34fb..c21348c80 100644 --- a/spec/unit/event-mapper.spec.ts +++ b/spec/unit/event-mapper.spec.ts @@ -29,7 +29,7 @@ describe("eventMapperFor", function() { client = new MatrixClient({ baseUrl: "https://my.home.server", accessToken: "my.access.token", - request: function() {} as any, // NOP + fetchFn: function() {} as any, // NOP store: { getRoom(roomId: string): Room | null { return rooms.find(r => r.roomId === roomId); diff --git a/spec/unit/http-api/__snapshots__/index.spec.ts.snap b/spec/unit/http-api/__snapshots__/index.spec.ts.snap new file mode 100644 index 000000000..e6487ddeb --- /dev/null +++ b/spec/unit/http-api/__snapshots__/index.spec.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = ` +{ + "base": "http://baseUrl", + "params": { + "access_token": "token", + }, + "path": "/_matrix/media/r0/upload", +} +`; diff --git a/spec/unit/http-api/fetch.spec.ts b/spec/unit/http-api/fetch.spec.ts new file mode 100644 index 000000000..e100f2d93 --- /dev/null +++ b/spec/unit/http-api/fetch.spec.ts @@ -0,0 +1,223 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { FetchHttpApi } from "../../../src/http-api/fetch"; +import { TypedEventEmitter } from "../../../src/models/typed-event-emitter"; +import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src"; +import { emitPromise } from "../../test-utils/test-utils"; + +describe("FetchHttpApi", () => { + const baseUrl = "http://baseUrl"; + const idBaseUrl = "http://idBaseUrl"; + const prefix = ClientPrefix.V3; + + it("should support aborting multiple times", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new FetchHttpApi(new TypedEventEmitter(), { baseUrl, prefix, fetchFn }); + + api.request(Method.Get, "/foo"); + api.request(Method.Get, "/baz"); + expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy(); + expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy(); + expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy(); + expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy(); + + api.abort(); + expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy(); + expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy(); + + api.request(Method.Get, "/bar"); + expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy(); + expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy(); + + api.abort(); + expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy(); + }); + + it("should fall back to global fetch if fetchFn not provided", () => { + global.fetch = jest.fn(); + expect(global.fetch).not.toHaveBeenCalled(); + const api = new FetchHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + api.fetch("test"); + expect(global.fetch).toHaveBeenCalled(); + }); + + it("should update identity server base url", () => { + const api = new FetchHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + expect(api.opts.idBaseUrl).toBeUndefined(); + api.setIdBaseUrl("https://id.foo.bar"); + expect(api.opts.idBaseUrl).toBe("https://id.foo.bar"); + }); + + describe("idServerRequest", () => { + it("should throw if no idBaseUrl", () => { + const api = new FetchHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2)) + .toThrow("No identity server base URL set"); + }); + + it("should send params as query string for GET requests", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new FetchHttpApi(new TypedEventEmitter(), { baseUrl, idBaseUrl, prefix, fetchFn }); + api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2); + expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar"); + expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]); + }); + + it("should send params as body for non-GET requests", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new FetchHttpApi(new TypedEventEmitter(), { baseUrl, idBaseUrl, prefix, fetchFn }); + const params = { foo: "bar", via: ["a", "b"] }; + api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2); + expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar"); + expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params); + }); + + it("should add Authorization header if token provided", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new FetchHttpApi(new TypedEventEmitter(), { baseUrl, idBaseUrl, prefix, fetchFn }); + api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token"); + expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token"); + }); + }); + + it("should return the Response object if onlyData=false", async () => { + const res = { ok: true }; + const fetchFn = jest.fn().mockResolvedValue(res); + const api = new FetchHttpApi(new TypedEventEmitter(), { baseUrl, prefix, fetchFn, onlyData: false }); + await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res); + }); + + it("should return text if json=false", async () => { + const text = "418 I'm a teapot"; + const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) }); + const api = new FetchHttpApi(new TypedEventEmitter(), { baseUrl, prefix, fetchFn, onlyData: true }); + await expect(api.requestOtherUrl(Method.Get, "http://url", undefined, { + json: false, + })).resolves.toBe(text); + }); + + it("should send token via query params if useAuthorizationHeader=false", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new FetchHttpApi(new TypedEventEmitter(), { + baseUrl, + prefix, + fetchFn, + accessToken: "token", + useAuthorizationHeader: false, + }); + api.authedRequest(Method.Get, "/path"); + expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token"); + }); + + it("should send token via headers by default", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new FetchHttpApi(new TypedEventEmitter(), { + baseUrl, + prefix, + fetchFn, + accessToken: "token", + }); + api.authedRequest(Method.Get, "/path"); + expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token"); + }); + + it("should not send a token if not calling `authedRequest`", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new FetchHttpApi(new TypedEventEmitter(), { + baseUrl, + prefix, + fetchFn, + accessToken: "token", + }); + api.request(Method.Get, "/path"); + expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy(); + expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy(); + }); + + it("should ensure no token is leaked out via query params if sending via headers", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new FetchHttpApi(new TypedEventEmitter(), { + baseUrl, + prefix, + fetchFn, + accessToken: "token", + useAuthorizationHeader: true, + }); + api.authedRequest(Method.Get, "/path", { access_token: "123" }); + expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy(); + expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token"); + }); + + it("should not override manually specified access token via query params", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new FetchHttpApi(new TypedEventEmitter(), { + baseUrl, + prefix, + fetchFn, + accessToken: "token", + useAuthorizationHeader: false, + }); + api.authedRequest(Method.Get, "/path", { access_token: "RealToken" }); + expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken"); + }); + + it("should not override manually specified access token via header", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new FetchHttpApi(new TypedEventEmitter(), { + baseUrl, + prefix, + fetchFn, + accessToken: "token", + useAuthorizationHeader: true, + }); + api.authedRequest(Method.Get, "/path", undefined, undefined, { + headers: { Authorization: "Bearer RealToken" }, + }); + expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken"); + }); + + it("should not override Accept header", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new FetchHttpApi(new TypedEventEmitter(), { baseUrl, prefix, fetchFn }); + api.authedRequest(Method.Get, "/path", undefined, undefined, { + headers: { Accept: "text/html" }, + }); + expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html"); + }); + + it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => { + const fetchFn = jest.fn().mockResolvedValue({ + ok: false, + headers: { + get(name: string): string | null { + return name === "Content-Type" ? "application/json" : null; + }, + }, + text: jest.fn().mockResolvedValue(JSON.stringify({ + errcode: "M_CONSENT_NOT_GIVEN", + error: "Ye shall ask for consent", + })), + }); + const emitter = new TypedEventEmitter(); + const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn }); + + await Promise.all([ + emitPromise(emitter, HttpApiEvent.NoConsent), + expect(api.authedRequest(Method.Get, "/path")).rejects.toThrow("Ye shall ask for consent"), + ]); + }); +}); diff --git a/spec/unit/http-api/index.spec.ts b/spec/unit/http-api/index.spec.ts new file mode 100644 index 000000000..89e122452 --- /dev/null +++ b/spec/unit/http-api/index.spec.ts @@ -0,0 +1,228 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import DOMException from "domexception"; +import { mocked } from "jest-mock"; + +import { ClientPrefix, MatrixHttpApi, Method, UploadResponse } from "../../../src"; +import { TypedEventEmitter } from "../../../src/models/typed-event-emitter"; + +type Writeable = { -readonly [P in keyof T]: T[P] }; + +jest.useFakeTimers(); + +describe("MatrixHttpApi", () => { + const baseUrl = "http://baseUrl"; + const prefix = ClientPrefix.V3; + + let xhr: Partial>; + let upload: Promise; + + const DONE = 0; + + global.DOMException = DOMException; + + beforeEach(() => { + xhr = { + upload: {} as XMLHttpRequestUpload, + open: jest.fn(), + send: jest.fn(), + abort: jest.fn(), + setRequestHeader: jest.fn(), + onreadystatechange: undefined, + getResponseHeader: jest.fn(), + }; + // We stub out XHR here as it is not available in JSDOM + // @ts-ignore + global.XMLHttpRequest = jest.fn().mockReturnValue(xhr); + // @ts-ignore + global.XMLHttpRequest.DONE = DONE; + }); + + afterEach(() => { + upload?.catch(() => {}); + // Abort any remaining requests + xhr.readyState = DONE; + xhr.status = 0; + // @ts-ignore + xhr.onreadystatechange?.(new Event("test")); + }); + + it("should fall back to `fetch` where xhr is unavailable", () => { + global.XMLHttpRequest = undefined; + const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) }); + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix, fetchFn }); + upload = api.uploadContent({} as File); + expect(fetchFn).toHaveBeenCalled(); + }); + + it("should prefer xhr where available", () => { + const fetchFn = jest.fn().mockResolvedValue({ ok: true }); + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix, fetchFn }); + upload = api.uploadContent({} as File); + expect(fetchFn).not.toHaveBeenCalled(); + expect(xhr.open).toHaveBeenCalled(); + }); + + it("should send access token in query params if header disabled", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { + baseUrl, + prefix, + accessToken: "token", + useAuthorizationHeader: false, + }); + upload = api.uploadContent({} as File); + expect(xhr.open) + .toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token"); + expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization"); + }); + + it("should send access token in header by default", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { + baseUrl, + prefix, + accessToken: "token", + }); + upload = api.uploadContent({} as File); + expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload"); + expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token"); + }); + + it("should include filename by default", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + upload = api.uploadContent({} as File, { name: "name" }); + expect(xhr.open) + .toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name"); + }); + + it("should allow not sending the filename", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + upload = api.uploadContent({} as File, { name: "name", includeFilename: false }); + expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload"); + }); + + it("should abort xhr when the upload is aborted", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + upload = api.uploadContent({} as File); + api.cancelUpload(upload); + expect(xhr.abort).toHaveBeenCalled(); + return expect(upload).rejects.toThrow("Aborted"); + }); + + it("should timeout if no progress in 30s", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + upload = api.uploadContent({} as File); + jest.advanceTimersByTime(25000); + // @ts-ignore + xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 })); + jest.advanceTimersByTime(25000); + expect(xhr.abort).not.toHaveBeenCalled(); + jest.advanceTimersByTime(5000); + expect(xhr.abort).toHaveBeenCalled(); + }); + + it("should call progressHandler", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + const progressHandler = jest.fn(); + upload = api.uploadContent({} as File, { progressHandler }); + const progressEvent = new Event("progress") as ProgressEvent; + Object.assign(progressEvent, { loaded: 1, total: 100 }); + // @ts-ignore + xhr.upload.onprogress(progressEvent); + expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 }); + + Object.assign(progressEvent, { loaded: 95, total: 100 }); + // @ts-ignore + xhr.upload.onprogress(progressEvent); + expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 }); + }); + + it("should error when no response body", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + upload = api.uploadContent({} as File); + + xhr.readyState = DONE; + xhr.responseText = ""; + xhr.status = 200; + // @ts-ignore + xhr.onreadystatechange?.(new Event("test")); + + return expect(upload).rejects.toThrow("No response body."); + }); + + it("should error on a 400-code", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + upload = api.uploadContent({} as File); + + xhr.readyState = DONE; + xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}'; + xhr.status = 404; + mocked(xhr.getResponseHeader).mockReturnValue("application/json"); + // @ts-ignore + xhr.onreadystatechange?.(new Event("test")); + + return expect(upload).rejects.toThrow("Not found"); + }); + + it("should return response on successful upload", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + upload = api.uploadContent({} as File); + + xhr.readyState = DONE; + xhr.responseText = '{"content_uri": "mxc://server/foobar"}'; + xhr.status = 200; + mocked(xhr.getResponseHeader).mockReturnValue("application/json"); + // @ts-ignore + xhr.onreadystatechange?.(new Event("test")); + + return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" }); + }); + + it("should abort xhr when calling `cancelUpload`", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + upload = api.uploadContent({} as File); + expect(api.cancelUpload(upload)).toBeTruthy(); + expect(xhr.abort).toHaveBeenCalled(); + }); + + it("should return false when `cancelUpload` is called but unsuccessful", async () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + upload = api.uploadContent({} as File); + + xhr.readyState = DONE; + xhr.status = 500; + mocked(xhr.getResponseHeader).mockReturnValue("application/json"); + // @ts-ignore + xhr.onreadystatechange?.(new Event("test")); + await upload.catch(() => {}); + + expect(api.cancelUpload(upload)).toBeFalsy(); + expect(xhr.abort).not.toHaveBeenCalled(); + }); + + it("should return active uploads in `getCurrentUploads`", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix }); + upload = api.uploadContent({} as File); + expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeTruthy(); + api.cancelUpload(upload); + expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeFalsy(); + }); + + it("should return expected object from `getContentUri`", () => { + const api = new MatrixHttpApi(new TypedEventEmitter(), { baseUrl, prefix, accessToken: "token" }); + expect(api.getContentUri()).toMatchSnapshot(); + }); +}); diff --git a/spec/unit/http-api/utils.spec.ts b/spec/unit/http-api/utils.spec.ts new file mode 100644 index 000000000..1a266c842 --- /dev/null +++ b/spec/unit/http-api/utils.spec.ts @@ -0,0 +1,183 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { mocked } from "jest-mock"; + +import { + anySignal, + ConnectionError, + MatrixError, + parseErrorResponse, + retryNetworkOperation, + timeoutSignal, +} from "../../../src"; +import { sleep } from "../../../src/utils"; + +jest.mock("../../../src/utils"); + +describe("timeoutSignal", () => { + jest.useFakeTimers(); + + it("should fire abort signal after specified timeout", () => { + const signal = timeoutSignal(3000); + const onabort = jest.fn(); + signal.onabort = onabort; + expect(signal.aborted).toBeFalsy(); + expect(onabort).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(3000); + expect(signal.aborted).toBeTruthy(); + expect(onabort).toHaveBeenCalled(); + }); +}); + +describe("anySignal", () => { + jest.useFakeTimers(); + + it("should fire when any signal fires", () => { + const { signal } = anySignal([ + timeoutSignal(3000), + timeoutSignal(2000), + ]); + + const onabort = jest.fn(); + signal.onabort = onabort; + expect(signal.aborted).toBeFalsy(); + expect(onabort).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(2000); + expect(signal.aborted).toBeTruthy(); + expect(onabort).toHaveBeenCalled(); + }); + + it("should cleanup when instructed", () => { + const { signal, cleanup } = anySignal([ + timeoutSignal(3000), + timeoutSignal(2000), + ]); + + const onabort = jest.fn(); + signal.onabort = onabort; + expect(signal.aborted).toBeFalsy(); + expect(onabort).not.toHaveBeenCalled(); + + cleanup(); + jest.advanceTimersByTime(2000); + expect(signal.aborted).toBeFalsy(); + expect(onabort).not.toHaveBeenCalled(); + }); + + it("should abort immediately if passed an aborted signal", () => { + const controller = new AbortController(); + controller.abort(); + const { signal } = anySignal([controller.signal]); + expect(signal.aborted).toBeTruthy(); + }); +}); + +describe("parseErrorResponse", () => { + it("should resolve Matrix Errors from XHR", () => { + expect(parseErrorResponse({ + getResponseHeader(name: string): string | null { + return name === "Content-Type" ? "application/json" : null; + }, + status: 500, + } as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({ + errcode: "TEST", + }, 500)); + }); + + it("should resolve Matrix Errors from fetch", () => { + expect(parseErrorResponse({ + headers: { + get(name: string): string | null { + return name === "Content-Type" ? "application/json" : null; + }, + }, + status: 500, + } as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({ + errcode: "TEST", + }, 500)); + }); + + it("should handle no type gracefully", () => { + expect(parseErrorResponse({ + headers: { + get(name: string): string | null { + return null; + }, + }, + status: 500, + } as Response, '{"errcode": "TEST"}')).toStrictEqual(new Error("Server returned 500 error")); + }); + + it("should handle invalid type gracefully", () => { + expect(parseErrorResponse({ + headers: { + get(name: string): string | null { + return name === "Content-Type" ? " " : null; + }, + }, + status: 500, + } as Response, '{"errcode": "TEST"}')) + .toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type")); + }); + + it("should handle plaintext errors", () => { + expect(parseErrorResponse({ + headers: { + get(name: string): string | null { + return name === "Content-Type" ? "text/plain" : null; + }, + }, + status: 418, + } as Response, "I'm a teapot")).toStrictEqual(new Error("Server returned 418 error: I'm a teapot")); + }); +}); + +describe("retryNetworkOperation", () => { + it("should retry given number of times with exponential sleeps", async () => { + const err = new ConnectionError("test"); + const fn = jest.fn().mockRejectedValue(err); + mocked(sleep).mockResolvedValue(undefined); + await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err); + expect(fn).toHaveBeenCalledTimes(4); + expect(mocked(sleep)).toHaveBeenCalledTimes(3); + expect(mocked(sleep).mock.calls[0][0]).toBe(2000); + expect(mocked(sleep).mock.calls[1][0]).toBe(4000); + expect(mocked(sleep).mock.calls[2][0]).toBe(8000); + }); + + it("should bail out on errors other than ConnectionError", async () => { + const err = new TypeError("invalid JSON"); + const fn = jest.fn().mockRejectedValue(err); + mocked(sleep).mockResolvedValue(undefined); + await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it("should return newest ConnectionError when giving up", async () => { + const err1 = new ConnectionError("test1"); + const err2 = new ConnectionError("test2"); + const err3 = new ConnectionError("test3"); + const errors = [err1, err2, err3]; + const fn = jest.fn().mockImplementation(() => { + throw errors.shift(); + }); + mocked(sleep).mockResolvedValue(undefined); + await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3); + }); +}); diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 2b8faf506..458c05f20 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -103,7 +103,7 @@ describe("MatrixClient", function() { ]; let acceptKeepalives: boolean; let pendingLookup = null; - function httpReq(cb, method, path, qp, data, prefix) { + function httpReq(method, path, qp, data, prefix) { if (path === KEEP_ALIVE_PATH && acceptKeepalives) { return Promise.resolve({ unstable_features: { @@ -132,7 +132,6 @@ describe("MatrixClient", function() { method: method, path: path, }; - pendingLookup.promise.abort = () => {}; // to make it a valid IAbortablePromise return pendingLookup.promise; } if (next.path === path && next.method === method) { @@ -178,7 +177,7 @@ describe("MatrixClient", function() { baseUrl: "https://my.home.server", idBaseUrl: identityServerUrl, accessToken: "my.access.token", - request: function() {} as any, // NOP + fetchFn: function() {} as any, // NOP store: store, scheduler: scheduler, userId: userId, @@ -1153,8 +1152,7 @@ describe("MatrixClient", function() { // event type combined const expectedEventType = M_BEACON_INFO.name; - const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0]; - expect(callback).toBeFalsy(); + const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0]; expect(method).toBe('PUT'); expect(path).toEqual( `/rooms/${encodeURIComponent(roomId)}/state/` + @@ -1168,7 +1166,7 @@ describe("MatrixClient", function() { await client.unstable_setLiveBeacon(roomId, content); // event type combined - const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0]; + const [, path, , requestContent] = client.http.authedRequest.mock.calls[0]; expect(path).toEqual( `/rooms/${encodeURIComponent(roomId)}/state/` + `${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`, @@ -1229,7 +1227,7 @@ describe("MatrixClient", function() { it("is called with plain text topic and callback and sends state event", async () => { const sendStateEvent = createSendStateEventMock("pizza"); client.sendStateEvent = sendStateEvent; - await client.setRoomTopic(roomId, "pizza", () => {}); + await client.setRoomTopic(roomId, "pizza"); expect(sendStateEvent).toHaveBeenCalledTimes(1); }); @@ -1244,15 +1242,9 @@ describe("MatrixClient", function() { describe("setPassword", () => { const auth = { session: 'abcdef', type: 'foo' }; const newPassword = 'newpassword'; - const callback = () => {}; - const passwordTest = (expectedRequestContent: any, expectedCallback?: Function) => { - const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0]; - if (expectedCallback) { - expect(callback).toBe(expectedCallback); - } else { - expect(callback).toBeFalsy(); - } + const passwordTest = (expectedRequestContent: any) => { + const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0]; expect(method).toBe('POST'); expect(path).toEqual('/account/password'); expect(queryParams).toBeFalsy(); @@ -1269,8 +1261,8 @@ describe("MatrixClient", function() { }); it("no logout_devices specified + callback", async () => { - await client.setPassword(auth, newPassword, callback); - passwordTest({ auth, new_password: newPassword }, callback); + await client.setPassword(auth, newPassword); + passwordTest({ auth, new_password: newPassword }); }); it("overload logoutDevices=true", async () => { @@ -1279,8 +1271,8 @@ describe("MatrixClient", function() { }); it("overload logoutDevices=true + callback", async () => { - await client.setPassword(auth, newPassword, true, callback); - passwordTest({ auth, new_password: newPassword, logout_devices: true }, callback); + await client.setPassword(auth, newPassword, true); + passwordTest({ auth, new_password: newPassword, logout_devices: true }); }); it("overload logoutDevices=false", async () => { @@ -1289,8 +1281,8 @@ describe("MatrixClient", function() { }); it("overload logoutDevices=false + callback", async () => { - await client.setPassword(auth, newPassword, false, callback); - passwordTest({ auth, new_password: newPassword, logout_devices: false }, callback); + await client.setPassword(auth, newPassword, false); + passwordTest({ auth, new_password: newPassword, logout_devices: false }); }); }); @@ -1305,8 +1297,7 @@ describe("MatrixClient", function() { const result = await client.getLocalAliases(roomId); // Current version of the endpoint we support is v3 - const [callback, method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0]; - expect(callback).toBeFalsy(); + const [method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0]; expect(data).toBeFalsy(); expect(method).toBe('GET'); expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`); diff --git a/spec/unit/models/MSC3089TreeSpace.spec.ts b/spec/unit/models/MSC3089TreeSpace.spec.ts index fdc8101eb..ef099fede 100644 --- a/spec/unit/models/MSC3089TreeSpace.spec.ts +++ b/spec/unit/models/MSC3089TreeSpace.spec.ts @@ -890,9 +890,8 @@ describe("MSC3089TreeSpace", () => { expect(contents.length).toEqual(fileContents.length); expect(opts).toMatchObject({ includeFilename: false, - onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this. }); - return Promise.resolve(mxc); + return Promise.resolve({ content_uri: mxc }); }); client.uploadContent = uploadFn; @@ -950,9 +949,8 @@ describe("MSC3089TreeSpace", () => { expect(contents.length).toEqual(fileContents.length); expect(opts).toMatchObject({ includeFilename: false, - onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this. }); - return Promise.resolve(mxc); + return Promise.resolve({ content_uri: mxc }); }); client.uploadContent = uploadFn; diff --git a/spec/unit/pusher.spec.ts b/spec/unit/pusher.spec.ts index 4a27ef55b..dd46770a4 100644 --- a/spec/unit/pusher.spec.ts +++ b/spec/unit/pusher.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import MockHttpBackend from 'matrix-mock-request'; -import { IHttpOpts, MatrixClient, PUSHER_ENABLED } from "../../src/matrix"; +import { MatrixClient, PUSHER_ENABLED } from "../../src/matrix"; import { mkPusher } from '../test-utils/test-utils'; const realSetTimeout = setTimeout; @@ -35,7 +35,7 @@ describe("Pushers", () => { client = new MatrixClient({ baseUrl: "https://my.home.server", accessToken: "my.access.token", - request: httpBackend.requestFn as unknown as IHttpOpts["request"], + fetchFn: httpBackend.fetchFn as typeof global.fetch, }); }); diff --git a/spec/unit/queueToDevice.spec.ts b/spec/unit/queueToDevice.spec.ts index a1ae2bcfe..c5b1f8a29 100644 --- a/spec/unit/queueToDevice.spec.ts +++ b/spec/unit/queueToDevice.spec.ts @@ -17,7 +17,7 @@ limitations under the License. import MockHttpBackend from 'matrix-mock-request'; import { indexedDB as fakeIndexedDB } from 'fake-indexeddb'; -import { IHttpOpts, IndexedDBStore, MatrixEvent, MemoryStore, Room } from "../../src"; +import { IndexedDBStore, MatrixEvent, MemoryStore, Room } from "../../src"; import { MatrixClient } from "../../src/client"; import { ToDeviceBatch } from '../../src/models/ToDeviceMessage'; import { logger } from '../../src/logger'; @@ -89,7 +89,7 @@ describe.each([ client = new MatrixClient({ baseUrl: "https://my.home.server", accessToken: "my.access.token", - request: httpBackend.requestFn as IHttpOpts["request"], + fetchFn: httpBackend.fetchFn as typeof global.fetch, store, }); }); diff --git a/spec/unit/read-receipt.spec.ts b/spec/unit/read-receipt.spec.ts index 2e906a3cc..07acaa184 100644 --- a/spec/unit/read-receipt.spec.ts +++ b/spec/unit/read-receipt.spec.ts @@ -18,7 +18,6 @@ import MockHttpBackend from 'matrix-mock-request'; import { ReceiptType } from '../../src/@types/read_receipts'; import { MatrixClient } from "../../src/client"; -import { IHttpOpts } from '../../src/http-api'; import { EventType } from '../../src/matrix'; import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt'; import { encodeUri } from '../../src/utils'; @@ -87,7 +86,7 @@ describe("Read receipt", () => { client = new MatrixClient({ baseUrl: "https://my.home.server", accessToken: "my.access.token", - request: httpBackend.requestFn as unknown as IHttpOpts["request"], + fetchFn: httpBackend.fetchFn as typeof global.fetch, }); client.isGuest = () => false; }); @@ -146,5 +145,23 @@ describe("Read receipt", () => { 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, false); + client.sendReceipt(threadEvent, ReceiptType.Read, undefined); + + await httpBackend.flushAllExpected(); + await flushPromises(); + }); }); }); diff --git a/spec/unit/utils.spec.ts b/spec/unit/utils.spec.ts index 5d804022e..1b11f2a7c 100644 --- a/spec/unit/utils.spec.ts +++ b/spec/unit/utils.spec.ts @@ -26,9 +26,7 @@ describe("utils", function() { foo: "bar", baz: "beer@", }; - expect(utils.encodeParams(params)).toEqual( - "foo=bar&baz=beer%40", - ); + expect(utils.encodeParams(params).toString()).toEqual("foo=bar&baz=beer%40"); }); it("should handle boolean and numeric values", function() { @@ -37,7 +35,24 @@ describe("utils", function() { number: 12345, boolean: false, }; - expect(utils.encodeParams(params)).toEqual("string=foobar&number=12345&boolean=false"); + expect(utils.encodeParams(params).toString()).toEqual("string=foobar&number=12345&boolean=false"); + }); + + it("should handle string arrays", () => { + const params = { + via: ["one", "two", "three"], + }; + expect(utils.encodeParams(params).toString()).toEqual("via=one&via=two&via=three"); + }); + }); + + describe("decodeParams", () => { + it("should be able to decode multiple values into an array", () => { + const params = "foo=bar&via=a&via=b&via=c"; + expect(utils.decodeParams(params)).toEqual({ + foo: "bar", + via: ["a", "b", "c"], + }); }); }); diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 679a6afba..6b5124349 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -30,6 +30,8 @@ declare global { namespace NodeJS { interface Global { localStorage: Storage; + // marker variable used to detect both the browser & node entrypoints being used at once + __js_sdk_entrypoint: unknown; } } diff --git a/src/@types/partials.ts b/src/@types/partials.ts index a729d80dc..bf27eab0e 100644 --- a/src/@types/partials.ts +++ b/src/@types/partials.ts @@ -40,11 +40,6 @@ export enum Preset { export type ResizeMethod = "crop" | "scale"; -// TODO move to http-api after TSification -export interface IAbortablePromise extends Promise { - abort(): void; -} - export type IdServerUnbindResult = "no-support" | "success"; // Knock and private are reserved keywords which are not yet implemented. diff --git a/src/@types/requests.ts b/src/@types/requests.ts index cb292bf2c..f9095455e 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Callback } from "../client"; import { IContent, IEvent } from "../models/event"; import { Preset, Visibility } from "./partials"; import { IEventWithRoomId, SearchKey } from "./search"; @@ -130,16 +129,6 @@ export interface IRoomDirectoryOptions { third_party_instance_id?: string; } -export interface IUploadOpts { - name?: string; - includeFilename?: boolean; - type?: string; - rawResponse?: boolean; - onlyContentUri?: boolean; - callback?: Callback; - progressHandler?: (state: {loaded: number, total: number}) => void; -} - export interface IAddThreePidOnlyBody { auth?: { type: string; diff --git a/src/ToDeviceMessageQueue.ts b/src/ToDeviceMessageQueue.ts index 12827d8bb..aaff912de 100644 --- a/src/ToDeviceMessageQueue.ts +++ b/src/ToDeviceMessageQueue.ts @@ -28,7 +28,7 @@ const MAX_BATCH_SIZE = 20; export class ToDeviceMessageQueue { private sending = false; private running = true; - private retryTimeout: number = null; + private retryTimeout: ReturnType | null = null; private retryAttempts = 0; constructor(private client: MatrixClient) { @@ -68,7 +68,7 @@ export class ToDeviceMessageQueue { logger.debug("Attempting to send queued to-device messages"); this.sending = true; - let headBatch; + let headBatch: IndexedToDeviceBatch; try { while (this.running) { headBatch = await this.client.store.getOldestToDeviceBatch(); @@ -92,7 +92,7 @@ export class ToDeviceMessageQueue { // bored and giving up for now if (Math.floor(e.httpStatus / 100) === 4) { logger.error("Fatal error when sending to-device message - dropping to-device batch!", e); - await this.client.store.removeToDeviceBatch(headBatch.id); + await this.client.store.removeToDeviceBatch(headBatch!.id); } else { logger.info("Automatic retry limit reached for to-device messages."); } diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index 71cbd2105..8bf87e517 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -17,10 +17,9 @@ limitations under the License. /** @module auto-discovery */ -import { ServerResponse } from "http"; - import { IClientWellKnown, IWellKnownConfig } from "./client"; import { logger } from './logger'; +import { MatrixError, Method, timeoutSignal } from "./http-api"; // Dev note: Auto discovery is part of the spec. // See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery @@ -395,6 +394,19 @@ export class AutoDiscovery { } } + private static fetch(resource: URL | string, options?: RequestInit): ReturnType { + if (this.fetchFn) { + return this.fetchFn(resource, options); + } + return global.fetch(resource, options); + } + + private static fetchFn?: typeof global.fetch; + + public static setFetchFn(fetchFn: typeof global.fetch): void { + AutoDiscovery.fetchFn = fetchFn; + } + /** * Fetches a JSON object from a given URL, as expected by all .well-known * related lookups. If the server gives a 404 then the `action` will be @@ -411,45 +423,55 @@ export class AutoDiscovery { * @return {Promise} Resolves to the returned state. * @private */ - private static fetchWellKnownObject(uri: string): Promise { - return new Promise((resolve) => { - // eslint-disable-next-line - const request = require("./matrix").getRequest(); - if (!request) throw new Error("No request library available"); - request( - { method: "GET", uri, timeout: 5000 }, - (error: Error, response: ServerResponse, body: string) => { - if (error || response?.statusCode < 200 || response?.statusCode >= 300) { - const result = { error, raw: {} }; - return resolve(response?.statusCode === 404 - ? { - ...result, - action: AutoDiscoveryAction.IGNORE, - reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN, - } : { - ...result, - action: AutoDiscoveryAction.FAIL_PROMPT, - reason: error?.message || "General failure", - }); - } + private static async fetchWellKnownObject(url: string): Promise { + let response: Response; - try { - return resolve({ - raw: JSON.parse(body), - action: AutoDiscoveryAction.SUCCESS, - }); - } catch (err) { - return resolve({ - error: err, - raw: {}, - action: AutoDiscoveryAction.FAIL_PROMPT, - reason: err?.name === "SyntaxError" - ? AutoDiscovery.ERROR_INVALID_JSON - : AutoDiscovery.ERROR_INVALID, - }); - } - }, - ); - }); + try { + response = await AutoDiscovery.fetch(url, { + method: Method.Get, + signal: timeoutSignal(5000), + }); + + if (response.status === 404) { + return { + raw: {}, + action: AutoDiscoveryAction.IGNORE, + reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN, + }; + } + + if (!response.ok) { + return { + raw: {}, + action: AutoDiscoveryAction.FAIL_PROMPT, + reason: "General failure", + }; + } + } catch (err) { + const error = err as Error | string | undefined; + return { + error, + raw: {}, + action: AutoDiscoveryAction.FAIL_PROMPT, + reason: (error)?.message || "General failure", + }; + } + + try { + return { + raw: await response.json(), + action: AutoDiscoveryAction.SUCCESS, + }; + } catch (err) { + const error = err as Error | string | undefined; + return { + error, + raw: {}, + action: AutoDiscoveryAction.FAIL_PROMPT, + reason: (error as MatrixError)?.name === "SyntaxError" + ? AutoDiscovery.ERROR_INVALID_JSON + : AutoDiscovery.ERROR_INVALID, + }; + } } } diff --git a/src/browser-index.js b/src/browser-index.js index 3e3627fa9..86e887bd4 100644 --- a/src/browser-index.js +++ b/src/browser-index.js @@ -14,25 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import request from "browser-request"; -import queryString from "qs"; - import * as matrixcs from "./matrix"; -if (matrixcs.getRequest()) { +if (global.__js_sdk_entrypoint) { throw new Error("Multiple matrix-js-sdk entrypoints detected!"); } - -matrixcs.request(function(opts, fn) { - // We manually fix the query string for browser-request because - // it doesn't correctly handle cases like ?via=one&via=two. Instead - // we mimic `request`'s query string interface to make it all work - // as expected. - // browser-request will happily take the constructed string as the - // query string without trying to modify it further. - opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions); - return request(opts, fn); -}); +global.__js_sdk_entrypoint = true; // just *accessing* indexedDB throws an exception in firefox with // indexeddb disabled. diff --git a/src/client.ts b/src/client.ts index a8857abbb..d8392ae92 100644 --- a/src/client.ts +++ b/src/client.ts @@ -36,7 +36,7 @@ import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, suppor import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter"; import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler'; import * as utils from './utils'; -import { sleep } from './utils'; +import { QueryDict, sleep } from './utils'; import { Direction, EventTimeline } from "./models/event-timeline"; import { IActionsObject, PushProcessor } from "./pushprocessor"; import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery"; @@ -49,22 +49,17 @@ import { IRoomEncryption, RoomList } from './crypto/RoomList'; import { logger } from './logger'; import { SERVICE_TYPES } from './service-types'; import { - FileType, HttpApiEvent, HttpApiEventHandlerMap, - IHttpOpts, - IUpload, + Upload, + UploadOpts, MatrixError, MatrixHttpApi, Method, - PREFIX_IDENTITY_V2, - PREFIX_MEDIA_R0, - PREFIX_R0, - PREFIX_UNSTABLE, - PREFIX_V1, - PREFIX_V3, retryNetworkOperation, - UploadContentResponseType, + ClientPrefix, + MediaPrefix, + IdentityPrefix, IHttpOpts, FileType, UploadResponse, } from "./http-api"; import { Crypto, @@ -154,7 +149,6 @@ import { IRoomDirectoryOptions, ISearchOpts, ISendEventResponse, - IUploadOpts, } from "./@types/requests"; import { EventType, @@ -168,7 +162,7 @@ import { UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, } from "./@types/event"; -import { IAbortablePromise, IdServerUnbindResult, IImageInfo, Preset, Visibility } from "./@types/partials"; +import { IdServerUnbindResult, IImageInfo, Preset, Visibility } from "./@types/partials"; import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper"; import { randomString } from "./randomstring"; import { BackupManager, IKeyBackup, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup"; @@ -209,7 +203,6 @@ import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature"; export type Store = IStore; -export type Callback = (err: Error | any | null, data?: T) => void; export type ResetTimelineCallback = (roomId: string) => boolean; const SCROLLBACK_DELAY_MS = 3000; @@ -262,12 +255,10 @@ export interface ICreateClientOpts { scheduler?: MatrixScheduler; /** - * The function to invoke for HTTP - * requests. The value of this property is typically require("request") - * as it returns a function which meets the required interface. See - * {@link requestFunction} for more information. + * The function to invoke for HTTP requests. + * Most supported environments have a global `fetch` registered to which this will fall back. */ - request?: IHttpOpts["request"]; + fetchFn?: typeof global.fetch; userId?: string; @@ -622,7 +613,7 @@ export interface IUploadKeysRequest { "org.matrix.msc2732.fallback_keys"?: Record; } -interface IOpenIDToken { +export interface IOpenIDToken { access_token: string; token_type: "Bearer" | string; matrix_server_name: string; @@ -930,15 +921,15 @@ export class MatrixClient extends TypedEventEmitter } = {}; public identityServer: IIdentityServerProvider; - public http: MatrixHttpApi; // XXX: Intended private, used in code. + public http: MatrixHttpApi; // XXX: Intended private, used in code. public crypto?: Crypto; // XXX: Intended private, used in code. public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code. public callEventHandler: CallEventHandler; // XXX: Intended private, used in code. @@ -1007,7 +998,7 @@ export class MatrixClient extends TypedEventEmitter[0], { + fetchFn: opts.fetchFn, baseUrl: opts.baseUrl, idBaseUrl: opts.idBaseUrl, accessToken: opts.accessToken, - request: opts.request, - prefix: PREFIX_R0, + prefix: ClientPrefix.R0, onlyData: true, extraParams: opts.queryParams, localTimeoutMs: opts.localTimeoutMs, @@ -1315,7 +1306,6 @@ export class MatrixClient extends TypedEventEmitter( - undefined, Method.Post, "/dehydrated_device/claim", undefined, @@ -1356,7 +1346,6 @@ export class MatrixClient extends TypedEventEmitter { try { return await this.http.authedRequest( - undefined, Method.Get, "/dehydrated_device", undefined, undefined, @@ -1649,9 +1638,7 @@ export class MatrixClient extends TypedEventEmitter { + return this.http.authedRequest(Method.Get, "/capabilities").catch((e: Error): void => { // We swallow errors because we need a default object anyhow logger.error(e); }).then((r: { capabilities?: ICapabilities } = {}) => { @@ -2271,7 +2258,7 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the number of sessions requiring backup + * @returns {Promise} Resolves to the number of sessions requiring backup */ public countSessionsNeedingBackup(): Promise { if (!this.crypto) { @@ -2683,8 +2670,8 @@ export class MatrixClient extends TypedEventEmitter( - undefined, Method.Get, "/room_keys/version", undefined, undefined, - { prefix: PREFIX_V3 }, + Method.Get, "/room_keys/version", undefined, undefined, + { prefix: ClientPrefix.V3 }, ); } catch (e) { if (e.errcode === 'M_NOT_FOUND') { @@ -2839,8 +2826,8 @@ export class MatrixClient extends TypedEventEmitter( - undefined, Method.Post, "/room_keys/version", undefined, data, - { prefix: PREFIX_V3 }, + Method.Post, "/room_keys/version", undefined, data, + { prefix: ClientPrefix.V3 }, ); // We could assume everything's okay and enable directly, but this ensures @@ -2854,7 +2841,7 @@ export class MatrixClient extends TypedEventEmitter { + public async deleteKeyBackupVersion(version: string): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2870,20 +2857,16 @@ export class MatrixClient extends TypedEventEmitter; - public sendKeyBackup(roomId: string, sessionId: undefined, version: string, data: IKeyBackup): Promise; - public sendKeyBackup(roomId: string, sessionId: string, version: string, data: IKeyBackup): Promise; + public sendKeyBackup( + roomId: undefined, + sessionId: undefined, + version: string | undefined, + data: IKeyBackup, + ): Promise; public sendKeyBackup( roomId: string, + sessionId: undefined, + version: string | undefined, + data: IKeyBackup, + ): Promise; + public sendKeyBackup( + roomId: string, + sessionId: string, + version: string | undefined, + data: IKeyBackup, + ): Promise; + public async sendKeyBackup( + roomId: string | undefined, sessionId: string | undefined, version: string | undefined, data: IKeyBackup, @@ -2924,9 +2922,9 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the number of sessions requiring a backup. + * @returns {Promise} Resolves to the number of sessions requiring a backup. */ public flagAllGroupSessionsForBackup(): Promise { if (!this.crypto) { @@ -3216,8 +3214,8 @@ export class MatrixClient extends TypedEventEmitter( - undefined, Method.Get, path.path, path.queryData, undefined, - { prefix: PREFIX_UNSTABLE }, + Method.Get, path.path, path.queryData, undefined, + { prefix: ClientPrefix.Unstable }, ); if ((res as IRoomsKeysResponse).rooms) { @@ -3267,22 +3265,18 @@ export class MatrixClient extends TypedEventEmitter; - public deleteKeysFromBackup(roomId: string, sessionId: undefined, version: string): Promise; - public deleteKeysFromBackup(roomId: string, sessionId: string, version: string): Promise; - public deleteKeysFromBackup( - roomId: string | undefined, - sessionId: string | undefined, - version: string, - ): Promise { + public deleteKeysFromBackup(roomId: undefined, sessionId: undefined, version?: string): Promise; + public deleteKeysFromBackup(roomId: string, sessionId: undefined, version?: string): Promise; + public deleteKeysFromBackup(roomId: string, sessionId: string, version?: string): Promise; + public async deleteKeysFromBackup(roomId?: string, sessionId?: string, version?: string): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } const path = this.makeKeyBackupPath(roomId, sessionId, version); - return this.http.authedRequest( - undefined, Method.Delete, path.path, path.queryData, undefined, - { prefix: PREFIX_UNSTABLE }, + await this.http.authedRequest( + Method.Delete, path.path, path.queryData, undefined, + { prefix: ClientPrefix.Unstable }, ); } @@ -3322,13 +3316,12 @@ export class MatrixClient extends TypedEventEmitter { + public getMediaConfig(): Promise { return this.http.authedRequest( - callback, Method.Get, "/config", undefined, undefined, { - prefix: PREFIX_MEDIA_R0, + Method.Get, "/config", undefined, undefined, { + prefix: MediaPrefix.R0, }, ); } @@ -3410,22 +3403,17 @@ export class MatrixClient extends TypedEventEmitter { + public setAccountData(eventType: EventType | string, content: IContent): Promise<{}> { const path = utils.encodeUri("/user/$userId/account_data/$type", { $userId: this.credentials.userId, $type: eventType, }); - const promise = retryNetworkOperation(5, () => { - return this.http.authedRequest(undefined, Method.Put, path, undefined, content); + return retryNetworkOperation(5, () => { + return this.http.authedRequest(Method.Put, path, undefined, content); }); - if (callback) { - promise.then(result => callback(null, result), callback); - } - return promise; } /** @@ -3442,11 +3430,10 @@ export class MatrixClient extends TypedEventEmitter(eventType: string): Promise { + public async getAccountDataFromServer(eventType: string): Promise { if (this.isInitialSyncComplete()) { const event = this.store.getAccountData(eventType); if (!event) { @@ -3454,14 +3441,14 @@ export class MatrixClient extends TypedEventEmitter(); } const path = utils.encodeUri("/user/$userId/account_data/$type", { $userId: this.credentials.userId, $type: eventType, }); try { - return await this.http.authedRequest(undefined, Method.Get, path); + return await this.http.authedRequest(Method.Get, path); } catch (e) { if (e.data?.errcode === 'M_NOT_FOUND') { return null; @@ -3483,16 +3470,15 @@ export class MatrixClient extends TypedEventEmitter { + public setIgnoredUsers(userIds: string[]): Promise<{}> { const content = { ignored_users: {} }; userIds.forEach((u) => { content.ignored_users[u] = {}; }); - return this.setAccountData("m.ignored_user_list", content, callback); + return this.setAccountData("m.ignored_user_list", content); } /** @@ -3513,31 +3499,25 @@ export class MatrixClient extends TypedEventEmitter Default: true. * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, the signing URL is passed in this parameter. * @param {string[]} opts.viaServers The server names to try and join through in addition to those that are automatically chosen. - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: Room object. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public async joinRoom(roomIdOrAlias: string, opts?: IJoinRoomOpts, callback?: Callback): Promise { - // to help people when upgrading.. - if (utils.isFunction(opts)) { - throw new Error("Expected 'opts' object, got function."); - } - opts = opts || {}; + public async joinRoom(roomIdOrAlias: string, opts: IJoinRoomOpts = {}): Promise { if (opts.syncRoom === undefined) { opts.syncRoom = true; } const room = this.getRoom(roomIdOrAlias); - if (room && room.hasMembershipState(this.credentials.userId, "join")) { + if (room?.hasMembershipState(this.credentials.userId, "join")) { return Promise.resolve(room); } let signPromise: Promise = Promise.resolve(); if (opts.inviteSignUrl) { - signPromise = this.http.requestOtherUrl( - undefined, Method.Post, - opts.inviteSignUrl, { mxid: this.credentials.userId }, + signPromise = this.http.requestOtherUrl( + Method.Post, + new URL(opts.inviteSignUrl), { mxid: this.credentials.userId }, ); } @@ -3546,8 +3526,6 @@ export class MatrixClient extends TypedEventEmitter { - return this.sendStateEvent(roomId, EventType.RoomName, { name: name }, undefined, callback); + public setRoomName(roomId: string, name: string): Promise { + return this.sendStateEvent(roomId, EventType.RoomName, { name: name }); } /** * @param {string} roomId * @param {string} topic * @param {string} htmlTopic Optional. - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3637,76 +3611,59 @@ export class MatrixClient extends TypedEventEmitter; - public setRoomTopic( - roomId: string, - topic: string, - callback?: Callback, - ): Promise; - public setRoomTopic( - roomId: string, - topic: string, - htmlTopicOrCallback?: string | Callback, ): Promise { - const isCallback = typeof htmlTopicOrCallback === 'function'; - const htmlTopic = isCallback ? undefined : htmlTopicOrCallback; - const callback = isCallback ? htmlTopicOrCallback : undefined; const content = ContentHelpers.makeTopicContent(topic, htmlTopic); - return this.sendStateEvent(roomId, EventType.RoomTopic, content, undefined, callback); + return this.sendStateEvent(roomId, EventType.RoomTopic, content); } /** * @param {string} roomId - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: to an object keyed by tagId with objects containing a numeric order field. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public getRoomTags(roomId: string, callback?: Callback): Promise { + public getRoomTags(roomId: string): Promise { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags", { $userId: this.credentials.userId, $roomId: roomId, }); - return this.http.authedRequest(callback, Method.Get, path); + return this.http.authedRequest(Method.Get, path); } /** * @param {string} roomId * @param {string} tagName name of room tag to be set * @param {object} metadata associated with that tag to be stored - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: to an empty object * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public setRoomTag(roomId: string, tagName: string, metadata: ITagMetadata, callback?: Callback): Promise<{}> { + public setRoomTag(roomId: string, tagName: string, metadata: ITagMetadata): Promise<{}> { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { $userId: this.credentials.userId, $roomId: roomId, $tag: tagName, }); - return this.http.authedRequest(callback, Method.Put, path, undefined, metadata); + return this.http.authedRequest(Method.Put, path, undefined, metadata); } /** * @param {string} roomId * @param {string} tagName name of room tag to be removed - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: void + * @return {Promise} Resolves: to an empty object * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public deleteRoomTag(roomId: string, tagName: string, callback?: Callback): Promise { + public deleteRoomTag(roomId: string, tagName: string): Promise<{}> { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { $userId: this.credentials.userId, $roomId: roomId, $tag: tagName, }); - return this.http.authedRequest(callback, Method.Delete, path); + return this.http.authedRequest(Method.Delete, path); } /** * @param {string} roomId * @param {string} eventType event type to be set * @param {object} content event content - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: to an empty object {} * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -3714,14 +3671,13 @@ export class MatrixClient extends TypedEventEmitter, - callback?: Callback, ): Promise<{}> { const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", { $userId: this.credentials.userId, $roomId: roomId, $type: eventType, }); - return this.http.authedRequest(callback, Method.Put, path, undefined, content); + return this.http.authedRequest(Method.Put, path, undefined, content); } /** @@ -3730,7 +3686,6 @@ export class MatrixClient extends TypedEventEmitter { let content = { users: {}, @@ -3753,7 +3707,7 @@ export class MatrixClient extends TypedEventEmitter; public sendEvent( roomId: string, @@ -3809,18 +3761,15 @@ export class MatrixClient extends TypedEventEmitter; public sendEvent( roomId: string, threadId: string | null, eventType: string | IContent, content: IContent | string, - txnId?: string | Callback, - callback?: Callback, + txnId?: string, ): Promise { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = txnId as Callback; txnId = content as string; content = eventType as IContent; eventType = threadId; @@ -3848,7 +3797,7 @@ export class MatrixClient extends TypedEventEmitter { - if (utils.isFunction(txnId)) { - callback = txnId as any as Callback; // convert for legacy - txnId = undefined; - } - if (!txnId) { txnId = this.makeTxnId(); } @@ -3929,18 +3871,17 @@ export class MatrixClient extends TypedEventEmitter { + private encryptAndSendEvent(room: Room, event: MatrixEvent): Promise { let cancelled = false; // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections, // so that we can handle synchronous and asynchronous exceptions with the @@ -3986,9 +3927,6 @@ export class MatrixClient extends TypedEventEmitter { - callback?.(null, res); - return res; }).catch(err => { logger.error("Error sending event", err.stack || err); try { @@ -4000,8 +3938,6 @@ export class MatrixClient extends TypedEventEmitter( - undefined, Method.Put, path, undefined, event.getWireContent(), + Method.Put, path, undefined, event.getWireContent(), ).then((res) => { logger.log(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`); return res; @@ -4120,11 +4056,8 @@ export class MatrixClient extends TypedEventEmitter; public redactEvent( roomId: string, threadId: string | null, eventId: string, txnId?: string | undefined, - cbOrOpts?: Callback | IRedactOpts, + opts?: IRedactOpts, ): Promise; public redactEvent( roomId: string, threadId: string | null, eventId?: string, - txnId?: string | Callback | IRedactOpts, - cbOrOpts?: Callback | IRedactOpts, + txnId?: string | IRedactOpts, + opts?: IRedactOpts, ): Promise { if (!eventId?.startsWith(EVENT_ID_PREFIX)) { - cbOrOpts = txnId as (Callback | IRedactOpts); + opts = txnId as IRedactOpts; txnId = eventId; eventId = threadId; threadId = null; } - const opts = typeof (cbOrOpts) === 'object' ? cbOrOpts : {}; - const reason = opts.reason; - const callback = typeof (cbOrOpts) === 'function' ? cbOrOpts : undefined; + const reason = opts?.reason; return this.sendCompleteEvent(roomId, threadId, { type: EventType.RoomRedaction, content: { reason }, redacts: eventId, - }, txnId as string, callback); + }, txnId as string); } /** @@ -4169,7 +4100,6 @@ export class MatrixClient extends TypedEventEmitter; public sendMessage( roomId: string, threadId: string | null, content: IContent, txnId?: string, - callback?: Callback, ): Promise; public sendMessage( roomId: string, threadId: string | null | IContent, content: IContent | string, - txnId?: string | Callback, - callback?: Callback, + txnId?: string, ): Promise { if (typeof threadId !== "string" && threadId !== null) { - callback = txnId as Callback; txnId = content as string; content = threadId as IContent; threadId = null; } - if (utils.isFunction(txnId)) { - callback = txnId as any as Callback; // for legacy - txnId = undefined; - } // Populate all outbound events with Extensible Events metadata to ensure there's a // reasonably large pool of messages to parse. @@ -4248,8 +4170,7 @@ export class MatrixClient extends TypedEventEmitter; public sendTextMessage( roomId: string, threadId: string | null, body: string, txnId?: string, - callback?: Callback, ): Promise; public sendTextMessage( roomId: string, threadId: string | null, body: string, - txnId?: string | Callback, - callback?: Callback, + txnId?: string, ): Promise { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = txnId as Callback; txnId = body; body = threadId; threadId = null; } const content = ContentHelpers.makeTextMessage(body); - return this.sendMessage(roomId, threadId, content, txnId as string, callback); + return this.sendMessage(roomId, threadId, content, txnId); } /** @@ -4297,7 +4213,6 @@ export class MatrixClient extends TypedEventEmitter; public sendNotice( roomId: string, threadId: string | null, body: string, txnId?: string, - callback?: Callback, ): Promise; public sendNotice( roomId: string, threadId: string | null, body: string, - txnId?: string | Callback, - callback?: Callback, + txnId?: string, ): Promise { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = txnId as Callback; txnId = body; body = threadId; threadId = null; } const content = ContentHelpers.makeNotice(body); - return this.sendMessage(roomId, threadId, content, txnId as string, callback); + return this.sendMessage(roomId, threadId, content, txnId); } /** @@ -4336,7 +4247,6 @@ export class MatrixClient extends TypedEventEmitter; public sendEmoteMessage( roomId: string, threadId: string | null, body: string, txnId?: string, - callback?: Callback, ): Promise; public sendEmoteMessage( roomId: string, threadId: string | null, body: string, - txnId?: string | Callback, - callback?: Callback, + txnId?: string, ): Promise { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = txnId as Callback; txnId = body; body = threadId; threadId = null; } const content = ContentHelpers.makeEmoteMessage(body); - return this.sendMessage(roomId, threadId, content, txnId as string, callback); + return this.sendMessage(roomId, threadId, content, txnId); } /** @@ -4376,7 +4282,6 @@ export class MatrixClient extends TypedEventEmitter; public sendImageMessage( roomId: string, @@ -4393,34 +4297,27 @@ export class MatrixClient extends TypedEventEmitter; public sendImageMessage( roomId: string, threadId: string | null, url: string | IImageInfo, info?: IImageInfo | string, - text: Callback | string = "Image", - callback?: Callback, + text = "Image", ): Promise { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = text as Callback; text = info as string || "Image"; info = url as IImageInfo; url = threadId as string; threadId = null; } - if (utils.isFunction(text)) { - callback = text as any as Callback; // legacy - text = undefined; - } const content = { msgtype: MsgType.Image, url: url, info: info, body: text, }; - return this.sendMessage(roomId, threadId, content, undefined, callback); + return this.sendMessage(roomId, threadId, content); } /** @@ -4429,7 +4326,6 @@ export class MatrixClient extends TypedEventEmitter; public sendStickerMessage( roomId: string, @@ -4446,34 +4341,27 @@ export class MatrixClient extends TypedEventEmitter; public sendStickerMessage( roomId: string, threadId: string | null, url: string | IImageInfo, info?: IImageInfo | string, - text: Callback | string = "Sticker", - callback?: Callback, + text = "Sticker", ): Promise { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = text as Callback; text = info as string || "Sticker"; info = url as IImageInfo; url = threadId as string; threadId = null; } - if (utils.isFunction(text)) { - callback = text as any as Callback; // legacy - text = undefined; - } const content = { url: url, info: info, body: text, }; - return this.sendEvent(roomId, threadId, EventType.Sticker, content, undefined, callback); + return this.sendEvent(roomId, threadId, EventType.Sticker, content); } /** @@ -4481,7 +4369,6 @@ export class MatrixClient extends TypedEventEmitter; public sendHtmlMessage( roomId: string, threadId: string | null, body: string, htmlBody: string, - callback?: Callback, ): Promise; public sendHtmlMessage( roomId: string, threadId: string | null, body: string, - htmlBody: string | Callback, - callback?: Callback, + htmlBody?: string, ): Promise { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = htmlBody as Callback; htmlBody = body as string; body = threadId; threadId = null; } - const content = ContentHelpers.makeHtmlMessage(body, htmlBody as string); - return this.sendMessage(roomId, threadId, content, undefined, callback); + const content = ContentHelpers.makeHtmlMessage(body, htmlBody); + return this.sendMessage(roomId, threadId, content); } /** * @param {string} roomId * @param {string} body * @param {string} htmlBody - * @param {module:client.callback} callback Optional. Deprecated * @return {Promise} Resolves: to a ISendEventResponse object * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -4527,30 +4409,26 @@ export class MatrixClient extends TypedEventEmitter; public sendHtmlNotice( roomId: string, threadId: string | null, body: string, htmlBody: string, - callback?: Callback, ): Promise; public sendHtmlNotice( roomId: string, threadId: string | null, body: string, - htmlBody: string | Callback, - callback?: Callback, + htmlBody?: string, ): Promise { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = htmlBody as Callback; htmlBody = body as string; body = threadId; threadId = null; } - const content = ContentHelpers.makeHtmlNotice(body, htmlBody as string); - return this.sendMessage(roomId, threadId, content, undefined, callback); + const content = ContentHelpers.makeHtmlNotice(body, htmlBody); + return this.sendMessage(roomId, threadId, content); } /** @@ -4558,7 +4436,6 @@ export class MatrixClient extends TypedEventEmitter; public sendHtmlEmote( roomId: string, threadId: string | null, body: string, htmlBody: string, - callback?: Callback, ): Promise; public sendHtmlEmote( roomId: string, threadId: string | null, body: string, - htmlBody: string | Callback, - callback?: Callback, + htmlBody?: string, ): Promise { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = htmlBody as Callback; htmlBody = body as string; body = threadId; threadId = null; } - const content = ContentHelpers.makeHtmlEmote(body, htmlBody as string); - return this.sendMessage(roomId, threadId, content, undefined, callback); + const content = ContentHelpers.makeHtmlEmote(body, htmlBody); + return this.sendMessage(roomId, threadId, content); } /** @@ -4598,7 +4471,6 @@ export class MatrixClient extends TypedEventEmitter { - if (typeof (body) === 'function') { - callback = body as any as Callback; // legacy - body = {}; - } - if (this.isGuest()) { return Promise.resolve({}); // guests cannot send receipts so don't bother. } @@ -4631,7 +4497,7 @@ export class MatrixClient extends TypedEventEmitter { if (!event) return; const eventId = event.getId(); @@ -4660,7 +4524,7 @@ export class MatrixClient extends TypedEventEmitter { + public getUrlPreview(url: string, ts: number): Promise { // bucket the timestamp to the nearest minute to prevent excessive spam to the server // Surely 60-second accuracy is enough for anyone. ts = Math.floor(ts / 60000) * 60000; @@ -4740,20 +4603,15 @@ export class MatrixClient extends TypedEventEmitter(Method.Get, "/preview_url", { + url, + ts: ts.toString(), + }, undefined, { + prefix: MediaPrefix.R0, + }); // TODO: Expire the URL preview cache sometimes this.urlPreviewCache[key] = resp; return resp; @@ -4763,11 +4621,10 @@ export class MatrixClient extends TypedEventEmitter { + public sendTyping(roomId: string, isTyping: boolean, timeoutMs: number): Promise<{}> { if (this.isGuest()) { return Promise.resolve({}); // guests cannot send typing notifications so don't bother. } @@ -4782,7 +4639,7 @@ export class MatrixClient extends TypedEventEmitter { - return this.membershipChange(roomId, userId, "invite", reason, callback); + public invite(roomId: string, userId: string, reason?: string): Promise<{}> { + return this.membershipChange(roomId, userId, "invite", reason); } /** * Invite a user to a room based on their email address. * @param {string} roomId The room to invite the user to. * @param {string} email The email address to invite. - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: {} an empty object. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public inviteByEmail(roomId: string, email: string, callback?: Callback): Promise<{}> { - return this.inviteByThreePid(roomId, "email", email, callback); + public inviteByEmail(roomId: string, email: string): Promise<{}> { + return this.inviteByThreePid(roomId, "email", email); } /** @@ -4892,11 +4747,10 @@ export class MatrixClient extends TypedEventEmitter { + public async inviteByThreePid(roomId: string, medium: string, address: string): Promise<{}> { const path = utils.encodeUri( "/rooms/$roomId/invite", { $roomId: roomId }, @@ -4925,17 +4779,16 @@ export class MatrixClient extends TypedEventEmitter { - return this.membershipChange(roomId, undefined, "leave", undefined, callback); + public leave(roomId: string): Promise<{}> { + return this.membershipChange(roomId, undefined, "leave"); } /** @@ -4989,28 +4842,22 @@ export class MatrixClient extends TypedEventEmitter { + return this.membershipChange(roomId, userId, "ban", reason); } /** * @param {string} roomId * @param {boolean} deleteRoom True to delete the room from the store on success. * Default: true. - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: {} an empty object. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public forget(roomId: string, deleteRoom?: boolean, callback?: Callback): Promise<{}> { - if (deleteRoom === undefined) { - deleteRoom = true; - } - const promise = this.membershipChange(roomId, undefined, "forget", undefined, - callback); + public forget(roomId: string, deleteRoom = true): Promise<{}> { + const promise = this.membershipChange(roomId, undefined, "forget"); if (!deleteRoom) { return promise; } @@ -5024,11 +4871,10 @@ export class MatrixClient extends TypedEventEmitter { + public unban(roomId: string, userId: string): Promise<{}> { // unbanning != set their state to leave: this used to be // the case, but was then changed so that leaving was always // a revoking of privilege, otherwise two people racing to @@ -5040,20 +4886,17 @@ export class MatrixClient extends TypedEventEmitter { + public kick(roomId: string, userId: string, reason?: string): Promise<{}> { const path = utils.encodeUri("/rooms/$roomId/kick", { $roomId: roomId, }); @@ -5061,9 +4904,7 @@ export class MatrixClient extends TypedEventEmitter { // API returns an empty object - if (utils.isFunction(reason)) { - callback = reason as any as Callback; // legacy - reason = undefined; - } - const path = utils.encodeUri("/rooms/$room_id/$membership", { $room_id: roomId, $membership: membership, }); return this.http.authedRequest( - callback, Method.Post, path, undefined, { + Method.Post, path, undefined, { user_id: userId, // may be undefined e.g. on leave reason: reason, }, @@ -5108,29 +4943,27 @@ export class MatrixClient extends TypedEventEmitter; - public setProfileInfo(info: "displayname", data: { displayname: string }, callback?: Callback): Promise<{}>; - public setProfileInfo(info: "avatar_url" | "displayname", data: object, callback?: Callback): Promise<{}> { + public setProfileInfo(info: "avatar_url", data: { avatar_url: string }): Promise<{}>; + public setProfileInfo(info: "displayname", data: { displayname: string }): Promise<{}>; + public setProfileInfo(info: "avatar_url" | "displayname", data: object): Promise<{}> { const path = utils.encodeUri("/profile/$userId/$info", { $userId: this.credentials.userId, $info: info, }); - return this.http.authedRequest(callback, Method.Put, path, undefined, data); + return this.http.authedRequest(Method.Put, path, undefined, data); } /** * @param {string} name - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: {} an empty object. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public async setDisplayName(name: string, callback?: Callback): Promise<{}> { - const prom = await this.setProfileInfo("displayname", { displayname: name }, callback); + public async setDisplayName(name: string): Promise<{}> { + const prom = await this.setProfileInfo("displayname", { displayname: name }); // XXX: synthesise a profile update for ourselves because Synapse is broken and won't const user = this.getUser(this.getUserId()); if (user) { @@ -5142,12 +4975,11 @@ export class MatrixClient extends TypedEventEmitter { - const prom = await this.setProfileInfo("avatar_url", { avatar_url: url }, callback); + public async setAvatarUrl(url: string): Promise<{}> { + const prom = await this.setProfileInfo("avatar_url", { avatar_url: url }); // XXX: synthesise a profile update for ourselves because Synapse is broken and won't const user = this.getUser(this.getUserId()); if (user) { @@ -5184,12 +5016,11 @@ export class MatrixClient extends TypedEventEmitter { + public async setPresence(opts: IPresenceOpts): Promise { const path = utils.encodeUri("/presence/$userId/status", { $userId: this.credentials.userId, }); @@ -5202,23 +5033,20 @@ export class MatrixClient extends TypedEventEmitter { + public getPresence(userId: string): Promise { const path = utils.encodeUri("/presence/$userId/status", { $userId: userId, }); - return this.http.authedRequest(callback, Method.Get, path); + return this.http.authedRequest(Method.Get, path); } /** @@ -5232,17 +5060,12 @@ export class MatrixClient extends TypedEventEmitterRoom.oldState.paginationToken will be * null. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public scrollback(room: Room, limit = 30, callback?: Callback): Promise { - if (utils.isFunction(limit)) { - callback = limit as any as Callback; // legacy - limit = undefined; - } + public scrollback(room: Room, limit = 30): Promise { let timeToWaitMs = 0; let info = this.ongoingScrollbacks[room.roomId] || {}; @@ -5294,13 +5117,11 @@ export class MatrixClient extends TypedEventEmitter { this.ongoingScrollbacks[room.roomId] = { errorTs: Date.now(), }; - callback?.(err); reject(err); }); }); @@ -5367,7 +5188,7 @@ export class MatrixClient extends TypedEventEmitter(undefined, Method.Get, path, params); + const res = await this.http.authedRequest(Method.Get, path, params); if (!res.event) { throw new Error("'event' not in '/context' result - homeserver too old?"); } @@ -5538,7 +5359,7 @@ export class MatrixClient extends TypedEventEmitter(undefined, Method.Get, path, params, undefined, opts) + return this.http.authedRequest(Method.Get, path, params, undefined, opts) .then(res => ({ ...res, start: res.prev_batch, @@ -5662,7 +5483,7 @@ export class MatrixClient extends TypedEventEmitter( - undefined, Method.Get, path, params, + Method.Get, path, params, ).then(async (res) => { const token = res.next_token; const matrixEvents: MatrixEvent[] = []; @@ -6007,7 +5828,6 @@ export class MatrixClient extends TypedEventEmitter { + public searchMessageText(opts: ISearchOpts): Promise { const roomEvents: ISearchRequestBody["search_categories"]["room_events"] = { search_term: opts.query, }; @@ -6204,7 +6024,7 @@ export class MatrixClient extends TypedEventEmitter(undefined, Method.Post, path, undefined, content) + return this.http.authedRequest(Method.Post, path, undefined, content) .then((response) => { // persist the filter const filter = Filter.fromJson(this.credentials.userId, response.filter_id, content); @@ -6402,7 +6222,7 @@ export class MatrixClient extends TypedEventEmitter(undefined, Method.Get, path).then((response) => { + return this.http.authedRequest(Method.Get, path).then((response) => { // persist the filter const filter = Filter.fromJson(userId, filterId, response); this.store.storeFilter(filter); @@ -6477,9 +6297,7 @@ export class MatrixClient extends TypedEventEmitter { @@ -6490,12 +6308,11 @@ export class MatrixClient extends TypedEventEmitter { - return this.http.authedRequest(callback, Method.Get, "/voip/turnServer"); + public turnServer(): Promise { + return this.http.authedRequest(Method.Get, "/voip/turnServer"); } /** @@ -6601,7 +6418,7 @@ export class MatrixClient extends TypedEventEmitter r['admin']); // pull out the specific boolean we want } @@ -6617,7 +6434,7 @@ export class MatrixClient extends TypedEventEmitter { @@ -6692,8 +6507,8 @@ export class MatrixClient extends TypedEventEmitter( - undefined, Method.Get, path, undefined, undefined, - { prefix: PREFIX_UNSTABLE }, + Method.Get, path, undefined, undefined, + { prefix: ClientPrefix.Unstable }, ); return res.joined; } @@ -6709,7 +6524,6 @@ export class MatrixClient extends TypedEventEmitter( - undefined, // callback Method.Get, "/_matrix/client/versions", undefined, // queryParams undefined, // data @@ -6997,12 +6811,12 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest<{ available: true }>( - undefined, Method.Get, '/register/available', { username }, + Method.Get, '/register/available', { username }, ).then((response) => { return response.available; }).catch(response => { @@ -7099,7 +6913,6 @@ export class MatrixClient extends TypedEventEmitter { // backwards compat if (bindThreepids === true) { @@ -7119,11 +6931,6 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types + public registerGuest(opts: { body?: any }): Promise { // TODO: Types opts = opts || {}; opts.body = opts.body || {}; - return this.registerRequest(opts.body, "guest", callback); + return this.registerRequest(opts.body, "guest"); } /** * @param {Object} data parameters for registration request * @param {string=} kind type of user to register. may be "guest" - * @param {module:client.callback=} callback * @return {Promise} Resolves: to the /register response * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public registerRequest(data: IRegisterRequestParams, kind?: string, callback?: Callback): Promise { + public registerRequest(data: IRegisterRequestParams, kind?: string): Promise { const params: { kind?: string } = {}; if (kind) { params.kind = kind; } - return this.http.request(callback, Method.Post, "/register", params, data); + return this.http.request(Method.Post, "/register", params, data); } /** @@ -7221,35 +7026,32 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest( - undefined, Method.Post, "/refresh", undefined, { refresh_token: refreshToken }, { - prefix: PREFIX_V1, + prefix: ClientPrefix.V1, inhibitLogoutEmit: true, // we don't want to cause logout loops }, ); } /** - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves to the available login flows * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public loginFlows(callback?: Callback): Promise { - return this.http.request(callback, Method.Get, "/login"); + public loginFlows(): Promise { + return this.http.request(Method.Get, "/login"); } /** * @param {string} loginType * @param {Object} data - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public login(loginType: string, data: any, callback?: Callback): Promise { // TODO: Types + public login(loginType: string, data: any): Promise { // TODO: Types const loginData = { type: loginType, }; @@ -7257,46 +7059,42 @@ export class MatrixClient extends TypedEventEmitter { - if (response && response.access_token && response.user_id) { - this.http.opts.accessToken = response.access_token; - this.credentials = { - userId: response.user_id, - }; - } - - if (callback) { - callback(error, response); - } - }, Method.Post, "/login", undefined, loginData, - ); + return this.http.authedRequest<{ + access_token?: string; + user_id?: string; + }>(Method.Post, "/login", undefined, loginData).then(response => { + if (response.access_token && response.user_id) { + this.http.opts.accessToken = response.access_token; + this.credentials = { + userId: response.user_id, + }; + } + return response; + }); } /** * @param {string} user * @param {string} password - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public loginWithPassword(user: string, password: string, callback?: Callback): Promise { // TODO: Types + public loginWithPassword(user: string, password: string): Promise { // TODO: Types return this.login("m.login.password", { user: user, password: password, - }, callback); + }); } /** * @param {string} relayState URL Callback after SAML2 Authentication - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public loginWithSAML2(relayState: string, callback?: Callback): Promise { // TODO: Types + public loginWithSAML2(relayState: string): Promise { // TODO: Types return this.login("m.login.saml2", { relay_state: relayState, - }, callback); + }); } /** @@ -7333,19 +7131,18 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types + public loginWithToken(token: string): Promise { // TODO: Types return this.login("m.login.token", { token: token, - }, callback); + }); } /** @@ -7354,11 +7151,10 @@ export class MatrixClient extends TypedEventEmitter { + public async logout(stopClient = false): Promise<{}> { if (this.crypto?.backupManager?.getKeyBackupEnabled()) { try { while (await this.crypto.backupManager.backupPendingKeys(200) > 0); @@ -7372,11 +7168,10 @@ export class MatrixClient extends TypedEventEmitter { - if (typeof (erase) === 'function') { - throw new Error('deactivateAccount no longer accepts a callback parameter'); - } - const body: any = {}; if (auth) { body.auth = auth; @@ -7404,7 +7195,7 @@ export class MatrixClient extends TypedEventEmitter> { const body: UIARequest<{}> = { auth }; return this.http.authedRequest( - undefined, // no callback support Method.Post, "/org.matrix.msc3882/login/token", undefined, // no query params body, - { prefix: PREFIX_UNSTABLE }, + { prefix: ClientPrefix.Unstable }, ); } @@ -7443,7 +7233,7 @@ export class MatrixClient extends TypedEventEmitter{room_id: {string}} * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public async createRoom( - options: ICreateRoomOpts, - callback?: Callback, - ): Promise<{ room_id: string }> { // eslint-disable-line camelcase + public async createRoom(options: ICreateRoomOpts): Promise<{ room_id: string }> { // eslint-disable-line camelcase // some valid options include: room_alias_name, visibility, invite // inject the id_access_token if inviting 3rd party addresses @@ -7481,7 +7267,7 @@ export class MatrixClient extends TypedEventEmitter { + public roomState(roomId: string): Promise { const path = utils.encodeUri("/rooms/$roomId/state", { $roomId: roomId }); - return this.http.authedRequest(callback, Method.Get, path); + return this.http.authedRequest(Method.Get, path); } /** * Get an event in a room by its event id. * @param {string} roomId * @param {string} eventId - * @param {module:client.callback} callback Optional. * * @return {Promise} Resolves to an object containing the event. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public fetchRoomEvent( - roomId: string, - eventId: string, - callback?: Callback, - ): Promise { + public fetchRoomEvent(roomId: string, eventId: string): Promise { const path = utils.encodeUri( "/rooms/$roomId/event/$eventId", { $roomId: roomId, $eventId: eventId, }, ); - return this.http.authedRequest(callback, Method.Get, path); + return this.http.authedRequest(Method.Get, path); } /** @@ -7567,7 +7347,6 @@ export class MatrixClient extends TypedEventEmitter { const queryParams: Record = {}; if (includeMembership) { @@ -7593,7 +7371,7 @@ export class MatrixClient extends TypedEventEmitter { // eslint-disable-line camelcase const path = utils.encodeUri("/rooms/$roomId/upgrade", { $roomId: roomId }); - return this.http.authedRequest( - undefined, Method.Post, path, undefined, { new_version: newVersion }, - ); + return this.http.authedRequest(Method.Post, path, undefined, { new_version: newVersion }); } /** @@ -7618,7 +7394,6 @@ export class MatrixClient extends TypedEventEmitter> { const pathParams = { $roomId: roomId, @@ -7637,9 +7411,7 @@ export class MatrixClient extends TypedEventEmitter { const pathParams = { $roomId: roomId, @@ -7667,27 +7437,21 @@ export class MatrixClient extends TypedEventEmitter { - if (utils.isFunction(limit)) { - callback = limit as any as Callback; // legacy - limit = undefined; - } - + public roomInitialSync(roomId: string, limit: number): Promise { const path = utils.encodeUri("/rooms/$roomId/initialSync", { $roomId: roomId }, ); - return this.http.authedRequest(callback, Method.Get, path, { limit: limit?.toString() ?? "30" }); + return this.http.authedRequest(Method.Get, path, { limit: limit?.toString() ?? "30" }); } /** @@ -7726,7 +7490,7 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/joined_rooms", {}); - return this.http.authedRequest(undefined, Method.Get, path); + return this.http.authedRequest(Method.Get, path); } /** @@ -7749,7 +7513,7 @@ export class MatrixClient extends TypedEventEmitter { - if (typeof (options) == 'function') { - callback = options; - options = {}; - } - if (options === undefined) { - options = {}; - } - - const queryParams: any = {}; - if (options.server) { - queryParams.server = options.server; - delete options.server; - } - - if (Object.keys(options).length === 0 && Object.keys(queryParams).length === 0) { - return this.http.authedRequest(callback, Method.Get, "/publicRooms"); + public publicRooms( + { server, limit, since, ...options }: IRoomDirectoryOptions = {}, + ): Promise { + const queryParams: QueryDict = { server, limit, since }; + if (Object.keys(options).length === 0) { + return this.http.authedRequest(Method.Get, "/publicRooms", queryParams); } else { - return this.http.authedRequest(callback, Method.Post, "/publicRooms", queryParams, options); + return this.http.authedRequest(Method.Post, "/publicRooms", queryParams, options); } } @@ -7791,33 +7543,31 @@ export class MatrixClient extends TypedEventEmitter { + public createAlias(alias: string, roomId: string): Promise<{}> { const path = utils.encodeUri("/directory/room/$alias", { $alias: alias, }); const data = { room_id: roomId, }; - return this.http.authedRequest(callback, Method.Put, path, undefined, data); + return this.http.authedRequest(Method.Put, path, undefined, data); } /** * Delete an alias to room ID mapping. This alias must be on your local server, * and you must have sufficient access to do this operation. * @param {string} alias The room alias to delete. - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: an empty object {}. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public deleteAlias(alias: string, callback?: Callback): Promise<{}> { + public deleteAlias(alias: string): Promise<{}> { const path = utils.encodeUri("/directory/room/$alias", { $alias: alias, }); - return this.http.authedRequest(callback, Method.Delete, path); + return this.http.authedRequest(Method.Delete, path); } /** @@ -7829,53 +7579,49 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId }); - const prefix = PREFIX_V3; - return this.http.authedRequest(undefined, Method.Get, path, undefined, undefined, { prefix }); + const prefix = ClientPrefix.V3; + return this.http.authedRequest(Method.Get, path, undefined, undefined, { prefix }); } /** * Get room info for the given alias. * @param {string} alias The room alias to resolve. - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: Object with room_id and servers. * @return {module:http-api.MatrixError} Rejects: with an error response. */ public getRoomIdForAlias( alias: string, - callback?: Callback, ): Promise<{ room_id: string, servers: string[] }> { // eslint-disable-line camelcase // TODO: deprecate this or resolveRoomAlias const path = utils.encodeUri("/directory/room/$alias", { $alias: alias, }); - return this.http.authedRequest(callback, Method.Get, path); + return this.http.authedRequest(Method.Get, path); } /** * @param {string} roomAlias - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: Object with room_id and servers. * @return {module:http-api.MatrixError} Rejects: with an error response. */ // eslint-disable-next-line camelcase - public resolveRoomAlias(roomAlias: string, callback?: Callback): Promise<{ room_id: string, servers: string[] }> { + public resolveRoomAlias(roomAlias: string): Promise<{ room_id: string, servers: string[] }> { // TODO: deprecate this or getRoomIdForAlias const path = utils.encodeUri("/directory/room/$alias", { $alias: roomAlias }); - return this.http.request(callback, Method.Get, path); + return this.http.request(Method.Get, path); } /** * Get the visibility of a room in the current HS's room directory * @param {string} roomId - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public getRoomDirectoryVisibility(roomId: string, callback?: Callback): Promise<{ visibility: Visibility }> { + public getRoomDirectoryVisibility(roomId: string): Promise<{ visibility: Visibility }> { const path = utils.encodeUri("/directory/list/room/$roomId", { $roomId: roomId, }); - return this.http.authedRequest(callback, Method.Get, path); + return this.http.authedRequest(Method.Get, path); } /** @@ -7884,15 +7630,14 @@ export class MatrixClient extends TypedEventEmitter { + public setRoomDirectoryVisibility(roomId: string, visibility: Visibility): Promise<{}> { const path = utils.encodeUri("/directory/list/room/$roomId", { $roomId: roomId, }); - return this.http.authedRequest(callback, Method.Put, path, undefined, { visibility }); + return this.http.authedRequest(Method.Put, path, undefined, { visibility }); } /** @@ -7904,7 +7649,6 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", { $networkId: networkId, $roomId: roomId, }); - return this.http.authedRequest( - callback, Method.Put, path, undefined, { "visibility": visibility }, - ); + return this.http.authedRequest(Method.Put, path, undefined, { "visibility": visibility }); } /** @@ -7940,7 +7681,7 @@ export class MatrixClient extends TypedEventEmitter( - file: FileType, - opts?: O, - ): IAbortablePromise> { - return this.http.uploadContent(file, opts); + public uploadContent(file: FileType, opts?: UploadOpts): Promise { + return this.http.uploadContent(file, opts); } /** * Cancel a file upload in progress - * @param {Promise} promise The promise returned from uploadContent + * @param {Promise} upload The object returned from uploadContent * @return {boolean} true if canceled, otherwise false */ - public cancelUpload(promise: IAbortablePromise): boolean { - return this.http.cancelUpload(promise); + public cancelUpload(upload: Promise): boolean { + return this.http.cancelUpload(upload); } /** @@ -8007,7 +7741,7 @@ export class MatrixClient extends TypedEventEmitter { - if (utils.isFunction(info)) { - callback = info as any as Callback; // legacy - info = undefined; - } - const path = info ? utils.encodeUri("/profile/$userId/$info", { $userId: userId, $info: info }) : utils.encodeUri("/profile/$userId", { $userId: userId }); - return this.http.authedRequest(callback, Method.Get, path); + return this.http.authedRequest(Method.Get, path); } /** - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves to a list of the user's threepids. * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public getThreePids(callback?: Callback): Promise<{ threepids: IThreepid[] }> { - return this.http.authedRequest(callback, Method.Get, "/account/3pid"); + public getThreePids(): Promise<{ threepids: IThreepid[] }> { + return this.http.authedRequest(Method.Get, "/account/3pid"); } /** @@ -8056,19 +7782,16 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types + public addThreePid(creds: any, bind: boolean): Promise { // TODO: Types const path = "/account/3pid"; const data = { 'threePidCreds': creds, 'bind': bind, }; - return this.http.authedRequest( - callback, Method.Post, path, undefined, data, - ); + return this.http.authedRequest(Method.Post, path, undefined, data); } /** @@ -8085,8 +7808,8 @@ export class MatrixClient extends TypedEventEmitter { const path = "/account/3pid/add"; - const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE; - return this.http.authedRequest(undefined, Method.Post, path, undefined, data, { prefix }); + const prefix = await this.isVersionSupported("r0.6.0") ? ClientPrefix.R0 : ClientPrefix.Unstable; + return this.http.authedRequest(Method.Post, path, undefined, data, { prefix }); } /** @@ -8105,11 +7828,8 @@ export class MatrixClient extends TypedEventEmitter { const path = "/account/3pid/bind"; - const prefix = await this.isVersionSupported("r0.6.0") ? - PREFIX_R0 : PREFIX_UNSTABLE; - return this.http.authedRequest( - undefined, Method.Post, path, undefined, data, { prefix }, - ); + const prefix = await this.isVersionSupported("r0.6.0") ? ClientPrefix.R0 : ClientPrefix.Unstable; + return this.http.authedRequest(Method.Post, path, undefined, data, { prefix }); } /** @@ -8134,8 +7854,8 @@ export class MatrixClient extends TypedEventEmitter { const path = "/account/3pid/delete"; - return this.http.authedRequest(undefined, Method.Post, path, undefined, { medium, address }); + return this.http.authedRequest(Method.Post, path, undefined, { medium, address }); } /** @@ -8160,36 +7880,14 @@ export class MatrixClient extends TypedEventEmitter; - public setPassword( - authDict: any, - newPassword: string, - logoutDevices: boolean, - callback?: Callback, - ): Promise<{}>; - public setPassword( - authDict: any, - newPassword: string, - logoutDevices?: Callback | boolean, - callback?: Callback, + logoutDevices?: boolean, ): Promise<{}> { - if (typeof logoutDevices === 'function') { - callback = logoutDevices; - } - if (typeof logoutDevices !== 'boolean') { - // Use backwards compatible behaviour of not specifying logout_devices - // This way it is left up to the server: - logoutDevices = undefined; - } - const path = "/account/password"; const data = { 'auth': authDict, @@ -8197,9 +7895,7 @@ export class MatrixClient extends TypedEventEmitter( - callback, Method.Post, path, undefined, data, - ); + return this.http.authedRequest<{}>(Method.Post, path, undefined, data); } /** @@ -8208,7 +7904,7 @@ export class MatrixClient extends TypedEventEmitter { - return this.http.authedRequest(undefined, Method.Get, "/devices"); + return this.http.authedRequest(Method.Get, "/devices"); } /** @@ -8221,7 +7917,7 @@ export class MatrixClient extends TypedEventEmitter { - const response = await this.http.authedRequest(callback, Method.Get, "/pushers"); + public async getPushers(): Promise<{ pushers: IPusher[] }> { + const response = await this.http.authedRequest<{ pushers: IPusher[] }>(Method.Get, "/pushers"); // Migration path for clients that connect to a homeserver that does not support // MSC3881 yet, see https://github.com/matrix-org/matrix-spec-proposals/blob/kerry/remote-push-toggle/proposals/3881-remote-push-notification-toggling.md#migration @@ -8310,13 +8005,12 @@ export class MatrixClient extends TypedEventEmitter { + public setPusher(pusher: IPusherRequest): Promise<{}> { const path = "/pushers/set"; - return this.http.authedRequest(callback, Method.Post, path, undefined, pusher); + return this.http.authedRequest(Method.Post, path, undefined, pusher); } /** @@ -8336,12 +8030,11 @@ export class MatrixClient extends TypedEventEmitter { - return this.http.authedRequest(callback, Method.Get, "/pushrules/").then((rules: IPushRules) => { + public getPushRules(): Promise { + return this.http.authedRequest(Method.Get, "/pushrules/").then((rules: IPushRules) => { return PushProcessor.rewriteDefaultRules(rules); }); } @@ -8351,7 +8044,6 @@ export class MatrixClient extends TypedEventEmitter, body: Pick, - callback?: Callback, ): Promise<{}> { // NB. Scope not uri encoded because devices need the '/' const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { $kind: kind, $ruleId: ruleId, }); - return this.http.authedRequest(callback, Method.Put, path, undefined, body); + return this.http.authedRequest(Method.Put, path, undefined, body); } /** * @param {string} scope * @param {string} kind * @param {string} ruleId - * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: an empty object {} * @return {module:http-api.MatrixError} Rejects: with an error response. */ @@ -8382,14 +8072,13 @@ export class MatrixClient extends TypedEventEmitter, - callback?: Callback, ): Promise<{}> { // NB. Scope not uri encoded because devices need the '/' const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { $kind: kind, $ruleId: ruleId, }); - return this.http.authedRequest(callback, Method.Delete, path); + return this.http.authedRequest(Method.Delete, path); } /** @@ -8398,7 +8087,6 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", { $kind: kind, $ruleId: ruleId, }); - return this.http.authedRequest( - callback, Method.Put, path, undefined, { "enabled": enabled }, - ); + return this.http.authedRequest(Method.Put, path, undefined, { "enabled": enabled }); } /** @@ -8424,7 +8109,6 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", { $kind: kind, $ruleId: ruleId, }); - return this.http.authedRequest( - callback, Method.Put, path, undefined, { "actions": actions }, - ); + return this.http.authedRequest(Method.Put, path, undefined, { "actions": actions }); } /** @@ -8449,19 +8130,17 @@ export class MatrixClient extends TypedEventEmitter { const queryParams: any = {}; if (opts.next_batch) { queryParams.next_batch = opts.next_batch; } - return this.http.authedRequest(callback, Method.Post, "/search", queryParams, opts.body); + return this.http.authedRequest(Method.Post, "/search", queryParams, opts.body); } /** @@ -8472,24 +8151,21 @@ export class MatrixClient extends TypedEventEmitter { - return this.http.authedRequest(callback, Method.Post, "/keys/upload", undefined, content); + return this.http.authedRequest(Method.Post, "/keys/upload", undefined, content); } public uploadKeySignatures(content: KeySignatures): Promise { return this.http.authedRequest( - undefined, Method.Post, '/keys/signatures/upload', undefined, + Method.Post, '/keys/signatures/upload', undefined, content, { - prefix: PREFIX_UNSTABLE, + prefix: ClientPrefix.Unstable, }, ); } @@ -8507,13 +8183,7 @@ export class MatrixClient extends TypedEventEmitter { - if (utils.isFunction(opts)) { - // opts used to be 'callback'. - throw new Error('downloadKeysForUsers no longer accepts a callback parameter'); - } - opts = opts || {}; - + public downloadKeysForUsers(userIds: string[], opts: { token?: string } = {}): Promise { const content: any = { device_keys: {}, }; @@ -8524,7 +8194,7 @@ export class MatrixClient extends TypedEventEmitter { // API returns empty object const data = Object.assign({}, keys); if (auth) Object.assign(data, { auth }); return this.http.authedRequest( - undefined, Method.Post, "/keys/device_signing/upload", undefined, data, { - prefix: PREFIX_UNSTABLE, + Method.Post, "/keys/device_signing/upload", undefined, data, { + prefix: ClientPrefix.Unstable, }, ); } @@ -8613,11 +8283,8 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types const params = { @@ -8660,8 +8325,8 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types const params = { @@ -8710,8 +8373,8 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types - const params = { - sid: sid, - client_secret: clientSecret, - token: msisdnToken, - }; - - return this.http.requestOtherUrl( - undefined, Method.Post, url, undefined, params, - ); + const u = new URL(url); + u.searchParams.set("sid", sid); + u.searchParams.set("client_secret", clientSecret); + u.searchParams.set("token", msisdnToken); + return this.http.requestOtherUrl(Method.Post, u); } /** @@ -8795,8 +8454,8 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types return this.http.idServerRequest( - undefined, Method.Get, "/hash_details", - null, PREFIX_IDENTITY_V2, identityAccessToken, + Method.Get, "/hash_details", + null, IdentityPrefix.V2, identityAccessToken, ); } @@ -8864,8 +8523,8 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types // Note: we're using the V2 API by calling this function, but our @@ -8912,7 +8569,6 @@ export class MatrixClient extends TypedEventEmitter p.address === address); if (!result) { - if (callback) callback(null, {}); return {}; } @@ -8928,7 +8584,6 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types return this.http.idServerRequest( - undefined, Method.Get, "/account", - undefined, PREFIX_IDENTITY_V2, identityAccessToken, + Method.Get, "/account", + undefined, IdentityPrefix.V2, identityAccessToken, ); } @@ -9020,7 +8675,7 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest>( - undefined, Method.Get, "/thirdparty/protocols", + Method.Get, "/thirdparty/protocols", ).then((response) => { // sanity check if (!response || typeof (response) !== 'object') { @@ -9067,7 +8722,7 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types const url = this.termsUrlForService(serviceType, baseUrl); - return this.http.requestOtherUrl(undefined, Method.Get, url); + return this.http.requestOtherUrl(Method.Get, url); } public agreeToTerms( @@ -9098,10 +8753,13 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types const url = this.termsUrlForService(serviceType, baseUrl); + utils.encodeParams({ + user_accepts: termsUrls, + }, url.searchParams); const headers = { Authorization: "Bearer " + accessToken, }; - return this.http.requestOtherUrl(undefined, Method.Post, url, null, { user_accepts: termsUrls }, { headers }); + return this.http.requestOtherUrl(Method.Post, url, null, { headers }); } /** @@ -9118,7 +8776,7 @@ export class MatrixClient extends TypedEventEmitter(undefined, Method.Get, path, queryParams, undefined, { - prefix: PREFIX_V1, + return this.http.authedRequest(Method.Get, path, queryParams, undefined, { + prefix: ClientPrefix.V1, }).catch(e => { if (e.errcode === "M_UNRECOGNIZED") { // fall back to the prefixed hierarchy API. - return this.http.authedRequest(undefined, Method.Get, path, queryParams, undefined, { + return this.http.authedRequest(Method.Get, path, queryParams, undefined, { prefix: "/_matrix/client/unstable/org.matrix.msc2946", }); } @@ -9179,7 +8837,7 @@ export class MatrixClient extends TypedEventEmitter { + req: MSC3575SlidingSyncRequest, + proxyBaseUrl?: string, + abortSignal?: AbortSignal, + ): Promise { const qps: Record = {}; if (req.pos) { qps.pos = req.pos; @@ -9252,7 +8913,6 @@ export class MatrixClient extends TypedEventEmitter( - undefined, Method.Post, "/sync", qps, @@ -9261,6 +8921,7 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias }); - return this.http.authedRequest(undefined, Method.Get, path, { via }, undefined, { - qsStringifyOptions: { arrayFormat: 'repeat' }, + return this.http.authedRequest(Method.Get, path, { via }, undefined, { prefix: "/_matrix/client/unstable/im.nheko.summary", }); } @@ -9314,7 +8974,7 @@ export class MatrixClient extends TypedEventEmitter { // eslint-disable-line camelcase - return this.http.authedRequest(undefined, Method.Get, "/account/whoami"); + return this.http.authedRequest(Method.Get, "/account/whoami"); } /** @@ -9333,7 +8993,6 @@ export class MatrixClient extends TypedEventEmitter( - undefined, Method.Put, "/dehydrated_device", undefined, @@ -273,7 +272,6 @@ export class DehydrationManager { logger.log("Uploading keys to server"); await this.crypto.baseApis.http.authedRequest( - undefined, Method.Post, "/keys/upload/" + encodeURI(deviceId), undefined, diff --git a/src/http-api.ts b/src/http-api.ts deleted file mode 100644 index 1a7376b00..000000000 --- a/src/http-api.ts +++ /dev/null @@ -1,1140 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 - 2021 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. -*/ - -/** - * This is an internal module. See {@link MatrixHttpApi} for the public class. - * @module http-api - */ - -import { parse as parseContentType, ParsedMediaType } from "content-type"; - -import type { IncomingHttpHeaders, IncomingMessage } from "http"; -import type { Request as _Request, CoreOptions } from "request"; -// we use our own implementation of setTimeout, so that if we get suspended in -// the middle of a /sync, we cancel the sync as soon as we awake, rather than -// waiting for the delay to elapse. -import * as callbacks from "./realtime-callbacks"; -import { IUploadOpts } from "./@types/requests"; -import { IAbortablePromise, IUsageLimit } from "./@types/partials"; -import { IDeferred, sleep } from "./utils"; -import { Callback } from "./client"; -import * as utils from "./utils"; -import { logger } from './logger'; -import { TypedEventEmitter } from "./models/typed-event-emitter"; - -/* -TODO: -- CS: complete register function (doing stages) -- Identity server: linkEmail, authEmail, bindEmail, lookup3pid -*/ - -/** - * A constant representing the URI path for release 0 of the Client-Server HTTP API. - */ -export const PREFIX_R0 = "/_matrix/client/r0"; - -/** - * A constant representing the URI path for the legacy release v1 of the Client-Server HTTP API. - */ -export const PREFIX_V1 = "/_matrix/client/v1"; - -/** - * A constant representing the URI path for Client-Server API endpoints versioned at v3. - */ -export const PREFIX_V3 = "/_matrix/client/v3"; - -/** - * A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs. - */ -export const PREFIX_UNSTABLE = "/_matrix/client/unstable"; - -/** - * URI path for v1 of the the identity API - * @deprecated Use v2. - */ -export const PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1"; - -/** - * URI path for the v2 identity API - */ -export const PREFIX_IDENTITY_V2 = "/_matrix/identity/v2"; - -/** - * URI path for the media repo API - */ -export const PREFIX_MEDIA_R0 = "/_matrix/media/r0"; - -type RequestProps = "method" - | "withCredentials" - | "json" - | "headers" - | "qs" - | "body" - | "qsStringifyOptions" - | "useQuerystring" - | "timeout"; - -export interface IHttpOpts { - baseUrl: string; - idBaseUrl?: string; - prefix: string; - onlyData: boolean; - accessToken?: string; - extraParams?: Record; - localTimeoutMs?: number; - useAuthorizationHeader?: boolean; - request(opts: Pick & { - uri: string; - method: Method; - // eslint-disable-next-line camelcase - _matrix_opts: IHttpOpts; - }, callback: RequestCallback): IRequest; -} - -interface IRequest extends _Request { - onprogress?(e: unknown): void; -} - -interface IRequestOpts { - prefix?: string; - baseUrl?: string; - localTimeoutMs?: number; - headers?: Record; - json?: boolean; // defaults to true - qsStringifyOptions?: CoreOptions["qsStringifyOptions"]; - bodyParser?(body: string): T; - - // Set to true to prevent the request function from emitting - // a Session.logged_out event. This is intended for use on - // endpoints where M_UNKNOWN_TOKEN is a valid/notable error - // response, such as with token refreshes. - inhibitLogoutEmit?: boolean; -} - -export interface IUpload { - loaded: number; - total: number; - promise: IAbortablePromise; -} - -interface IContentUri { - base: string; - path: string; - params: { - // eslint-disable-next-line camelcase - access_token: string; - }; -} - -type ResponseType | void = void> = - O extends { bodyParser: (body: string) => T } ? T : - O extends { json: false } ? string : - T; - -interface IUploadResponse { - // eslint-disable-next-line camelcase - content_uri: string; -} - -// This type's defaults only work for the Browser -// in the Browser we default rawResponse = false & onlyContentUri = true -// in Node we default rawResponse = true & onlyContentUri = false -export type UploadContentResponseType = - O extends undefined ? string : - O extends { rawResponse: true } ? string : - O extends { onlyContentUri: true } ? string : - O extends { rawResponse: false } ? IUploadResponse : - O extends { onlyContentUri: false } ? IUploadResponse : - string; - -export enum Method { - Get = "GET", - Put = "PUT", - Post = "POST", - Delete = "DELETE", -} - -export type FileType = Document | XMLHttpRequestBodyInit; - -export enum HttpApiEvent { - SessionLoggedOut = "Session.logged_out", - NoConsent = "no_consent", -} - -export type HttpApiEventHandlerMap = { - [HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void; - [HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void; -}; - -/** - * Construct a MatrixHttpApi. - * @constructor - * @param {EventEmitter} eventEmitter The event emitter to use for emitting events - * @param {Object} opts The options to use for this HTTP API. - * @param {string} opts.baseUrl Required. The base client-server URL e.g. - * 'http://localhost:8008'. - * @param {Function} opts.request Required. The function to call for HTTP - * requests. This function must look like function(opts, callback){ ... }. - * @param {string} opts.prefix Required. The matrix client prefix to use, e.g. - * '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants. - * - * @param {boolean} opts.onlyData True to return only the 'data' component of the - * response (e.g. the parsed HTTP body). If false, requests will return an - * object with the properties code, headers and data. - * - * @param {string=} opts.accessToken The access_token to send with requests. Can be - * null to not send an access token. - * @param {Object=} opts.extraParams Optional. Extra query parameters to send on - * requests. - * @param {Number=} opts.localTimeoutMs The default maximum amount of time to wait - * before timing out the request. If not specified, there is no timeout. - * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use - * Authorization header instead of query param to send the access token to the server. - */ -export class MatrixHttpApi { - private uploads: IUpload[] = []; - - constructor( - private eventEmitter: TypedEventEmitter, - public readonly opts: IHttpOpts, - ) { - utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]); - opts.onlyData = !!opts.onlyData; - opts.useAuthorizationHeader = !!opts.useAuthorizationHeader; - } - - /** - * Sets the base URL for the identity server - * @param {string} url The new base url - */ - public setIdBaseUrl(url: string): void { - this.opts.idBaseUrl = url; - } - - /** - * Get the content repository url with query parameters. - * @return {Object} An object with a 'base', 'path' and 'params' for base URL, - * path and query parameters respectively. - */ - public getContentUri(): IContentUri { - return { - base: this.opts.baseUrl, - path: "/_matrix/media/r0/upload", - params: { - access_token: this.opts.accessToken, - }, - }; - } - - /** - * Upload content to the homeserver - * - * @param {object} file The object to upload. On a browser, something that - * can be sent to XMLHttpRequest.send (typically a File). Under node.js, - * a Buffer, String or ReadStream. - * - * @param {object} opts options object - * - * @param {string=} opts.name Name to give the file on the server. Defaults - * to file.name. - * - * @param {boolean=} opts.includeFilename if false will not send the filename, - * e.g for encrypted file uploads where filename leaks are undesirable. - * Defaults to true. - * - * @param {string=} opts.type Content-type for the upload. Defaults to - * file.type, or applicaton/octet-stream. - * - * @param {boolean=} opts.rawResponse Return the raw body, rather than - * parsing the JSON. Defaults to false (except on node.js, where it - * defaults to true for backwards compatibility). - * - * @param {boolean=} opts.onlyContentUri Just return the content URI, - * rather than the whole body. Defaults to false (except on browsers, - * where it defaults to true for backwards compatibility). Ignored if - * opts.rawResponse is true. - * - * @param {Function=} opts.callback Deprecated. Optional. The callback to - * invoke on success/failure. See the promise return values for more - * information. - * - * @param {Function=} opts.progressHandler Optional. Called when a chunk of - * data has been uploaded, with an object containing the fields `loaded` - * (number of bytes transferred) and `total` (total size, if known). - * - * @return {Promise} Resolves to response object, as - * determined by this.opts.onlyData, opts.rawResponse, and - * opts.onlyContentUri. Rejects with an error (usually a MatrixError). - */ - public uploadContent( - file: FileType, - opts?: O, - ): IAbortablePromise> { - if (utils.isFunction(opts)) { - // opts used to be callback, backwards compatibility - opts = { - callback: opts as unknown as IUploadOpts["callback"], - } as O; - } else if (!opts) { - opts = {} as O; - } - - // default opts.includeFilename to true (ignoring falsey values) - const includeFilename = opts.includeFilename !== false; - - // if the file doesn't have a mime type, use a default since - // the HS errors if we don't supply one. - const contentType = opts.type || (file as File).type || 'application/octet-stream'; - const fileName = opts.name || (file as File).name; - - // We used to recommend setting file.stream to the thing to upload on - // Node.js. As of 2019-06-11, this is still in widespread use in various - // clients, so we should preserve this for simple objects used in - // Node.js. File API objects (via either the File or Blob interfaces) in - // the browser now define a `stream` method, which leads to trouble - // here, so we also check the type of `stream`. - let body = file; - const bodyStream = (body as File | Blob).stream; // this type is wrong but for legacy reasons is good enough - if (bodyStream && typeof bodyStream !== "function") { - logger.warn( - "Using `file.stream` as the content to upload. Future " + - "versions of the js-sdk will change this to expect `file` to " + - "be the content directly.", - ); - body = bodyStream; - } - - // backwards-compatibility hacks where we used to do different things - // between browser and node. - let rawResponse = opts.rawResponse; - if (rawResponse === undefined) { - if (global.XMLHttpRequest) { - rawResponse = false; - } else { - logger.warn( - "Returning the raw JSON from uploadContent(). Future " + - "versions of the js-sdk will change this default, to " + - "return the parsed object. Set opts.rawResponse=false " + - "to change this behaviour now.", - ); - rawResponse = true; - } - } - - let onlyContentUri = opts.onlyContentUri; - if (!rawResponse && onlyContentUri === undefined) { - if (global.XMLHttpRequest) { - logger.warn( - "Returning only the content-uri from uploadContent(). " + - "Future versions of the js-sdk will change this " + - "default, to return the whole response object. Set " + - "opts.onlyContentUri=false to change this behaviour now.", - ); - onlyContentUri = true; - } else { - onlyContentUri = false; - } - } - - // browser-request doesn't support File objects because it deep-copies - // the options using JSON.parse(JSON.stringify(options)). Instead of - // loading the whole file into memory as a string and letting - // browser-request base64 encode and then decode it again, we just - // use XMLHttpRequest directly. - // (browser-request doesn't support progress either, which is also kind - // of important here) - - const upload = { loaded: 0, total: 0 } as IUpload; - let promise: IAbortablePromise>; - - // XMLHttpRequest doesn't parse JSON for us. request normally does, but - // we're setting opts.json=false so that it doesn't JSON-encode the - // request, which also means it doesn't JSON-decode the response. Either - // way, we have to JSON-parse the response ourselves. - let bodyParser: ((body: string) => any) | undefined; - if (!rawResponse) { - bodyParser = function(rawBody: string) { - let body = JSON.parse(rawBody); - if (onlyContentUri) { - body = body.content_uri; - if (body === undefined) { - throw Error('Bad response'); - } - } - return body; - }; - } - - if (global.XMLHttpRequest) { - const defer = utils.defer>(); - const xhr = new global.XMLHttpRequest(); - const cb = requestCallback(defer, opts.callback, this.opts.onlyData); - - const timeoutFn = function() { - xhr.abort(); - cb(new Error('Timeout')); - }; - - // set an initial timeout of 30s; we'll advance it each time we get a progress notification - let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000); - - xhr.onreadystatechange = function() { - let resp: string; - switch (xhr.readyState) { - case global.XMLHttpRequest.DONE: - callbacks.clearTimeout(timeoutTimer); - try { - if (xhr.status === 0) { - throw new AbortError(); - } - if (!xhr.responseText) { - throw new Error('No response body.'); - } - resp = xhr.responseText; - if (bodyParser) { - resp = bodyParser(resp); - } - } catch (err) { - err.httpStatus = xhr.status; - cb(err); - return; - } - cb(undefined, xhr, resp); - break; - } - }; - xhr.upload.addEventListener("progress", function(ev) { - callbacks.clearTimeout(timeoutTimer); - upload.loaded = ev.loaded; - upload.total = ev.total; - timeoutTimer = callbacks.setTimeout(timeoutFn, 30000); - if (opts.progressHandler) { - opts.progressHandler({ - loaded: ev.loaded, - total: ev.total, - }); - } - }); - let url = this.opts.baseUrl + "/_matrix/media/r0/upload"; - - const queryArgs = []; - - if (includeFilename && fileName) { - queryArgs.push("filename=" + encodeURIComponent(fileName)); - } - - if (!this.opts.useAuthorizationHeader) { - queryArgs.push("access_token=" + encodeURIComponent(this.opts.accessToken)); - } - - if (queryArgs.length > 0) { - url += "?" + queryArgs.join("&"); - } - - xhr.open("POST", url); - if (this.opts.useAuthorizationHeader) { - xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken); - } - xhr.setRequestHeader("Content-Type", contentType); - xhr.send(body); - promise = defer.promise as IAbortablePromise>; - - // dirty hack (as per doRequest) to allow the upload to be cancelled. - promise.abort = xhr.abort.bind(xhr); - } else { - const queryParams: Record = {}; - - if (includeFilename && fileName) { - queryParams.filename = fileName; - } - - const headers: Record = { "Content-Type": contentType }; - - // authedRequest uses `request` which is no longer maintained. - // `request` has a bug where if the body is zero bytes then you get an error: `Argument error, options.body`. - // See https://github.com/request/request/issues/920 - // if body looks like a byte array and empty then set the Content-Length explicitly as a workaround: - if ((body as unknown as ArrayLike).length === 0) { - headers["Content-Length"] = "0"; - } - - promise = this.authedRequest>( - opts.callback, Method.Post, "/upload", queryParams, body, { - prefix: "/_matrix/media/r0", - headers, - json: false, - bodyParser, - }, - ); - } - - // remove the upload from the list on completion - upload.promise = promise.finally(() => { - for (let i = 0; i < this.uploads.length; ++i) { - if (this.uploads[i] === upload) { - this.uploads.splice(i, 1); - return; - } - } - }) as IAbortablePromise>; - - // copy our dirty abort() method to the new promise - upload.promise.abort = promise.abort; - this.uploads.push(upload); - - return upload.promise as IAbortablePromise>; - } - - public cancelUpload(promise: IAbortablePromise): boolean { - if (promise.abort) { - promise.abort(); - return true; - } - return false; - } - - public getCurrentUploads(): IUpload[] { - return this.uploads; - } - - public idServerRequest( - callback: Callback, - method: Method, - path: string, - params: Record, - prefix: string, - accessToken: string, - ): Promise { - if (!this.opts.idBaseUrl) { - throw new Error("No identity server base URL set"); - } - - const fullUri = this.opts.idBaseUrl + prefix + path; - - if (callback !== undefined && !utils.isFunction(callback)) { - throw Error( - "Expected callback to be a function but got " + typeof callback, - ); - } - - const opts = { - uri: fullUri, - method, - withCredentials: false, - json: true, // we want a JSON response if we can - _matrix_opts: this.opts, - headers: {}, - } as Parameters[0]; - - if (method === Method.Get) { - opts.qs = params; - } else if (typeof params === "object") { - opts.json = params; - } - - if (accessToken) { - opts.headers['Authorization'] = `Bearer ${accessToken}`; - } - - const defer = utils.defer(); - this.opts.request(opts, requestCallback(defer, callback, this.opts.onlyData)); - return defer.promise; - } - - /** - * Perform an authorised request to the homeserver. - * @param {Function} callback Optional. The callback to invoke on - * success/failure. See the promise return values for more information. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} path The HTTP path after the supplied prefix e.g. - * "/createRoom". - * - * @param {Object=} queryParams A dict of query params (these will NOT be - * urlencoded). If unspecified, there will be no query params. - * - * @param {Object} [data] The HTTP JSON body. - * - * @param {Object|Number=} opts additional options. If a number is specified, - * this is treated as `opts.localTimeoutMs`. - * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. - * - * @param {string=} opts.prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. - * - * @param {string=} opts.baseUrl The alternative base url to use. - * If not specified, uses this.opts.baseUrl - * - * @param {Object=} opts.headers map of additional request headers - * - * @return {Promise} Resolves to {data: {Object}, - * headers: {Object}, code: {Number}}. - * If onlyData is set, this will resolve to the data - * object only. - * @return {module:http-api.MatrixError} Rejects with an error if a problem - * occurred. This includes network problems and Matrix-specific error JSON. - */ - public authedRequest = IRequestOpts>( - callback: Callback | undefined, - method: Method, - path: string, - queryParams?: Record, - data?: CoreOptions["body"], - opts?: O | number, // number is legacy - ): IAbortablePromise> { - if (!queryParams) queryParams = {}; - let requestOpts = (opts || {}) as O; - - if (this.opts.useAuthorizationHeader) { - if (isFinite(opts as number)) { - // opts used to be localTimeoutMs - requestOpts = { - localTimeoutMs: opts as number, - } as O; - } - - if (!requestOpts.headers) { - requestOpts.headers = {}; - } - if (!requestOpts.headers.Authorization) { - requestOpts.headers.Authorization = "Bearer " + this.opts.accessToken; - } - if (queryParams.access_token) { - delete queryParams.access_token; - } - } else if (!queryParams.access_token) { - queryParams.access_token = this.opts.accessToken; - } - - const requestPromise = this.request(callback, method, path, queryParams, data, requestOpts); - - requestPromise.catch((err: MatrixError) => { - if (err.errcode == 'M_UNKNOWN_TOKEN' && !requestOpts?.inhibitLogoutEmit) { - this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err); - } else if (err.errcode == 'M_CONSENT_NOT_GIVEN') { - this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri); - } - }); - - // return the original promise, otherwise tests break due to it having to - // go around the event loop one more time to process the result of the request - return requestPromise; - } - - /** - * Perform a request to the homeserver without any credentials. - * @param {Function} callback Optional. The callback to invoke on - * success/failure. See the promise return values for more information. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} path The HTTP path after the supplied prefix e.g. - * "/createRoom". - * - * @param {Object=} queryParams A dict of query params (these will NOT be - * urlencoded). If unspecified, there will be no query params. - * - * @param {Object} [data] The HTTP JSON body. - * - * @param {Object=} opts additional options - * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. - * - * @param {string=} opts.prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. - * - * @param {Object=} opts.headers map of additional request headers - * - * @return {Promise} Resolves to {data: {Object}, - * headers: {Object}, code: {Number}}. - * If onlyData is set, this will resolve to the data - * object only. - * @return {module:http-api.MatrixError} Rejects with an error if a problem - * occurred. This includes network problems and Matrix-specific error JSON. - */ - public request = IRequestOpts>( - callback: Callback | undefined, - method: Method, - path: string, - queryParams?: CoreOptions["qs"], - data?: CoreOptions["body"], - opts?: O, - ): IAbortablePromise> { - const prefix = opts?.prefix ?? this.opts.prefix; - const baseUrl = opts?.baseUrl ?? this.opts.baseUrl; - const fullUri = baseUrl + prefix + path; - - return this.requestOtherUrl(callback, method, fullUri, queryParams, data, opts); - } - - /** - * Perform a request to an arbitrary URL. - * @param {Function} callback Optional. The callback to invoke on - * success/failure. See the promise return values for more information. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} uri The HTTP URI - * - * @param {Object=} queryParams A dict of query params (these will NOT be - * urlencoded). If unspecified, there will be no query params. - * - * @param {Object} [data] The HTTP JSON body. - * - * @param {Object=} opts additional options - * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. - * - * @param {string=} opts.prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. - * - * @param {Object=} opts.headers map of additional request headers - * - * @return {Promise} Resolves to {data: {Object}, - * headers: {Object}, code: {Number}}. - * If onlyData is set, this will resolve to the data - * object only. - * @return {module:http-api.MatrixError} Rejects with an error if a problem - * occurred. This includes network problems and Matrix-specific error JSON. - */ - public requestOtherUrl = IRequestOpts>( - callback: Callback | undefined, - method: Method, - uri: string, - queryParams?: CoreOptions["qs"], - data?: CoreOptions["body"], - opts?: O | number, // number is legacy - ): IAbortablePromise> { - let requestOpts = (opts || {}) as O; - if (isFinite(opts as number)) { - // opts used to be localTimeoutMs - requestOpts = { - localTimeoutMs: opts as number, - } as O; - } - - return this.doRequest(callback, method, uri, queryParams, data, requestOpts); - } - - /** - * Form and return a homeserver request URL based on the given path - * params and prefix. - * @param {string} path The HTTP path after the supplied prefix e.g. - * "/createRoom". - * @param {Object} queryParams A dict of query params (these will NOT be - * urlencoded). - * @param {string} prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". - * @return {string} URL - */ - public getUrl(path: string, queryParams: CoreOptions["qs"], prefix: string): string { - let queryString = ""; - if (queryParams) { - queryString = "?" + utils.encodeParams(queryParams); - } - return this.opts.baseUrl + prefix + path + queryString; - } - - /** - * @private - * - * @param {function} callback - * @param {string} method - * @param {string} uri - * @param {object} queryParams - * @param {object|string} data - * @param {object=} opts - * - * @param {boolean} [opts.json =true] Json-encode data before sending, and - * decode response on receipt. (We will still json-decode error - * responses, even if this is false.) - * - * @param {object=} opts.headers extra request headers - * - * @param {number=} opts.localTimeoutMs client-side timeout for the - * request. Default timeout if falsy. - * - * @param {function=} opts.bodyParser function to parse the body of the - * response before passing it to the promise and callback. - * - * @return {Promise} a promise which resolves to either the - * response object (if this.opts.onlyData is truthy), or the parsed - * body. Rejects - * - * Generic T is the callback/promise resolve type - * Generic O should be inferred - */ - private doRequest = IRequestOpts>( - callback: Callback | undefined, - method: Method, - uri: string, - queryParams?: Record, - data?: CoreOptions["body"], - opts?: O, - ): IAbortablePromise> { - if (callback !== undefined && !utils.isFunction(callback)) { - throw Error("Expected callback to be a function but got " + typeof callback); - } - - if (this.opts.extraParams) { - queryParams = { - ...(queryParams || {}), - ...this.opts.extraParams, - }; - } - - const headers = Object.assign({}, opts.headers || {}); - if (!opts) opts = {} as O; - const json = opts.json ?? true; - let bodyParser = opts.bodyParser; - - // we handle the json encoding/decoding here, because request and - // browser-request make a mess of it. Specifically, they attempt to - // json-decode plain-text error responses, which in turn means that the - // actual error gets swallowed by a SyntaxError. - - if (json) { - if (data) { - data = JSON.stringify(data); - headers['content-type'] = 'application/json'; - } - - if (!headers['accept']) { - headers['accept'] = 'application/json'; - } - - if (bodyParser === undefined) { - bodyParser = function(rawBody: string) { - return JSON.parse(rawBody); - }; - } - } - - const defer = utils.defer(); - - let timeoutId: number; - let timedOut = false; - let req: IRequest; - const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs; - - const resetTimeout = () => { - if (localTimeoutMs) { - if (timeoutId) { - callbacks.clearTimeout(timeoutId); - } - timeoutId = callbacks.setTimeout(function() { - timedOut = true; - req?.abort?.(); - defer.reject(new MatrixError({ - error: "Locally timed out waiting for a response", - errcode: "ORG.MATRIX.JSSDK_TIMEOUT", - timeout: localTimeoutMs, - })); - }, localTimeoutMs); - } - }; - resetTimeout(); - - const reqPromise = defer.promise as IAbortablePromise>; - - try { - req = this.opts.request( - { - uri: uri, - method: method, - withCredentials: false, - qs: queryParams, - qsStringifyOptions: opts.qsStringifyOptions, - useQuerystring: true, - body: data, - json: false, - timeout: localTimeoutMs, - headers: headers || {}, - _matrix_opts: this.opts, - }, - (err, response, body) => { - if (localTimeoutMs) { - callbacks.clearTimeout(timeoutId); - if (timedOut) { - return; // already rejected promise - } - } - - const handlerFn = requestCallback(defer, callback, this.opts.onlyData, bodyParser); - handlerFn(err, response, body); - }, - ); - if (req) { - // This will only work in a browser, where opts.request is the - // `browser-request` import. Currently, `request` does not support progress - // updates - see https://github.com/request/request/pull/2346. - // `browser-request` returns an XHRHttpRequest which exposes `onprogress` - if ('onprogress' in req) { - req.onprogress = (e) => { - // Prevent the timeout from rejecting the deferred promise if progress is - // seen with the request - resetTimeout(); - }; - } - - // FIXME: This is EVIL, but I can't think of a better way to expose - // abort() operations on underlying HTTP requests :( - if (req.abort) { - reqPromise.abort = req.abort.bind(req); - } - } - } catch (ex) { - defer.reject(ex); - if (callback) { - callback(ex); - } - } - return reqPromise; - } -} - -type RequestCallback = (err?: Error, response?: XMLHttpRequest | IncomingMessage, body?: string) => void; - -// if using onlyData=false then wrap your expected data type in this generic -export interface IResponse { - code: number; - data: T; - headers?: IncomingHttpHeaders; -} - -function getStatusCode(response: XMLHttpRequest | IncomingMessage): number { - return (response as XMLHttpRequest).status || (response as IncomingMessage).statusCode; -} - -/* - * Returns a callback that can be invoked by an HTTP request on completion, - * that will either resolve or reject the given defer as well as invoke the - * given userDefinedCallback (if any). - * - * HTTP errors are transformed into javascript errors and the deferred is rejected. - * - * If bodyParser is given, it is used to transform the body of the successful - * responses before passing to the defer/callback. - * - * If onlyData is true, the defer/callback is invoked with the body of the - * response, otherwise the result object (with `code` and `data` fields) - * - */ -function requestCallback( - defer: IDeferred, - userDefinedCallback?: Callback, - onlyData = false, - bodyParser?: (body: string) => T, -): RequestCallback { - return function(err: Error, response: XMLHttpRequest | IncomingMessage, body: string): void { - if (err) { - // the unit tests use matrix-mock-request, which throw the string "aborted" when aborting a request. - // See https://github.com/matrix-org/matrix-mock-request/blob/3276d0263a561b5b8326b47bae720578a2c7473a/src/index.js#L48 - const aborted = err.name === "AbortError" || (err as any as string) === "aborted"; - if (!aborted && !(err instanceof MatrixError)) { - // browser-request just throws normal Error objects, - // not `TypeError`s like fetch does. So just assume any - // error is due to the connection. - err = new ConnectionError("request failed", err); - } - } - - let data: T | string = body; - - if (!err) { - try { - if (getStatusCode(response) >= 400) { - err = parseErrorResponse(response, body); - } else if (bodyParser) { - data = bodyParser(body); - } - } catch (e) { - err = new Error(`Error parsing server response: ${e}`); - } - } - - if (err) { - defer.reject(err); - userDefinedCallback?.(err); - } else if (onlyData) { - defer.resolve(data as T); - userDefinedCallback?.(null, data as T); - } else { - const res: IResponse = { - code: getStatusCode(response), - - // XXX: why do we bother with this? it doesn't work for - // XMLHttpRequest, so clearly we don't use it. - headers: (response as IncomingMessage).headers, - data: data as T, - }; - // XXX: the variations in caller-expected types here are horrible, - // typescript doesn't do conditional types based on runtime values - defer.resolve(res as any as T); - userDefinedCallback?.(null, res as any as T); - } - }; -} - -/** - * Attempt to turn an HTTP error response into a Javascript Error. - * - * If it is a JSON response, we will parse it into a MatrixError. Otherwise - * we return a generic Error. - * - * @param {XMLHttpRequest|http.IncomingMessage} response response object - * @param {String} body raw body of the response - * @returns {Error} - */ -function parseErrorResponse(response: XMLHttpRequest | IncomingMessage, body?: string) { - const httpStatus = getStatusCode(response); - const contentType = getResponseContentType(response); - - let err; - if (contentType) { - if (contentType.type === 'application/json') { - const jsonBody = typeof(body) === 'object' ? body : JSON.parse(body); - err = new MatrixError(jsonBody); - } else if (contentType.type === 'text/plain') { - err = new Error(`Server returned ${httpStatus} error: ${body}`); - } - } - - if (!err) { - err = new Error(`Server returned ${httpStatus} error`); - } - err.httpStatus = httpStatus; - return err; -} - -/** - * extract the Content-Type header from the response object, and - * parse it to a `{type, parameters}` object. - * - * returns null if no content-type header could be found. - * - * @param {XMLHttpRequest|http.IncomingMessage} response response object - * @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found - */ -function getResponseContentType(response: XMLHttpRequest | IncomingMessage): ParsedMediaType { - let contentType; - if ((response as XMLHttpRequest).getResponseHeader) { - // XMLHttpRequest provides getResponseHeader - contentType = (response as XMLHttpRequest).getResponseHeader("Content-Type"); - } else if ((response as IncomingMessage).headers) { - // request provides http.IncomingMessage which has a message.headers map - contentType = (response as IncomingMessage).headers['content-type'] || null; - } - - if (!contentType) { - return null; - } - - try { - return parseContentType(contentType); - } catch (e) { - throw new Error(`Error parsing Content-Type '${contentType}': ${e}`); - } -} - -interface IErrorJson extends Partial { - [key: string]: any; // extensible - errcode?: string; - error?: string; -} - -/** - * Construct a Matrix error. This is a JavaScript Error with additional - * information specific to the standard Matrix error response. - * @constructor - * @param {Object} errorJson The Matrix error JSON returned from the homeserver. - * @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN". - * @prop {string} name Same as MatrixError.errcode but with a default unknown string. - * @prop {string} message The Matrix 'error' value, e.g. "Missing token." - * @prop {Object} data The raw Matrix error JSON used to construct this object. - * @prop {number} httpStatus The numeric HTTP status code given - */ -export class MatrixError extends Error { - public readonly errcode: string; - public readonly data: IErrorJson; - public httpStatus?: number; // set by http-api - - constructor(errorJson: IErrorJson = {}) { - super(`MatrixError: ${errorJson.errcode}`); - this.errcode = errorJson.errcode; - this.name = errorJson.errcode || "Unknown error code"; - this.message = errorJson.error || "Unknown message"; - this.data = errorJson; - } -} - -/** - * Construct a ConnectionError. This is a JavaScript Error indicating - * that a request failed because of some error with the connection, either - * CORS was not correctly configured on the server, the server didn't response, - * the request timed out, or the internet connection on the client side went down. - * @constructor - */ -export class ConnectionError extends Error { - constructor(message: string, cause: Error = undefined) { - super(message + (cause ? `: ${cause.message}` : "")); - } - - get name() { - return "ConnectionError"; - } -} - -export class AbortError extends Error { - constructor() { - super("Operation aborted"); - } - - get name() { - return "AbortError"; - } -} - -/** - * Retries a network operation run in a callback. - * @param {number} maxAttempts maximum attempts to try - * @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again. - * @return {any} the result of the network operation - * @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError - */ -export async function retryNetworkOperation(maxAttempts: number, callback: () => Promise): Promise { - let attempts = 0; - let lastConnectionError = null; - while (attempts < maxAttempts) { - try { - if (attempts > 0) { - const timeout = 1000 * Math.pow(2, attempts); - logger.log(`network operation failed ${attempts} times,` + - ` retrying in ${timeout}ms...`); - await sleep(timeout); - } - return callback(); - } catch (err) { - if (err instanceof ConnectionError) { - attempts += 1; - lastConnectionError = err; - } else { - throw err; - } - } - } - throw lastConnectionError; -} diff --git a/src/http-api/errors.ts b/src/http-api/errors.ts new file mode 100644 index 000000000..d44b23e7a --- /dev/null +++ b/src/http-api/errors.ts @@ -0,0 +1,64 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { IUsageLimit } from "../@types/partials"; + +interface IErrorJson extends Partial { + [key: string]: any; // extensible + errcode?: string; + error?: string; +} + +/** + * Construct a Matrix error. This is a JavaScript Error with additional + * information specific to the standard Matrix error response. + * @constructor + * @param {Object} errorJson The Matrix error JSON returned from the homeserver. + * @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN". + * @prop {string} name Same as MatrixError.errcode but with a default unknown string. + * @prop {string} message The Matrix 'error' value, e.g. "Missing token." + * @prop {Object} data The raw Matrix error JSON used to construct this object. + * @prop {number} httpStatus The numeric HTTP status code given + */ +export class MatrixError extends Error { + public readonly errcode?: string; + public readonly data: IErrorJson; + + constructor(errorJson: IErrorJson = {}, public httpStatus?: number) { + super(`MatrixError: ${errorJson.errcode}`); + this.errcode = errorJson.errcode; + this.name = errorJson.errcode || "Unknown error code"; + this.message = errorJson.error || "Unknown message"; + this.data = errorJson; + } +} + +/** + * Construct a ConnectionError. This is a JavaScript Error indicating + * that a request failed because of some error with the connection, either + * CORS was not correctly configured on the server, the server didn't response, + * the request timed out, or the internet connection on the client side went down. + * @constructor + */ +export class ConnectionError extends Error { + constructor(message: string, cause?: Error) { + super(message + (cause ? `: ${cause.message}` : "")); + } + + get name() { + return "ConnectionError"; + } +} diff --git a/src/http-api/fetch.ts b/src/http-api/fetch.ts new file mode 100644 index 000000000..4fecaaecf --- /dev/null +++ b/src/http-api/fetch.ts @@ -0,0 +1,327 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This is an internal module. See {@link MatrixHttpApi} for the public class. + * @module http-api + */ + +import * as utils from "../utils"; +import { TypedEventEmitter } from "../models/typed-event-emitter"; +import { Method } from "./method"; +import { ConnectionError, MatrixError } from "./errors"; +import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IRequestOpts } from "./interface"; +import { anySignal, parseErrorResponse, timeoutSignal } from "./utils"; +import { QueryDict } from "../utils"; + +type Body = Record | BodyInit; + +interface TypedResponse extends Response { + json(): Promise; +} + +export type ResponseType = + O extends undefined ? T : + O extends { onlyData: true } ? T : + TypedResponse; + +export class FetchHttpApi { + private abortController = new AbortController(); + + constructor( + private eventEmitter: TypedEventEmitter, + public readonly opts: O, + ) { + utils.checkObjectHasKeys(opts, ["baseUrl", "prefix"]); + opts.onlyData = !!opts.onlyData; + opts.useAuthorizationHeader = opts.useAuthorizationHeader ?? true; + } + + public abort(): void { + this.abortController.abort(); + this.abortController = new AbortController(); + } + + public fetch(resource: URL | string, options?: RequestInit): ReturnType { + if (this.opts.fetchFn) { + return this.opts.fetchFn(resource, options); + } + return global.fetch(resource, options); + } + + /** + * Sets the base URL for the identity server + * @param {string} url The new base url + */ + public setIdBaseUrl(url: string): void { + this.opts.idBaseUrl = url; + } + + public idServerRequest( + method: Method, + path: string, + params: Record, + prefix: string, + accessToken?: string, + ): Promise> { + if (!this.opts.idBaseUrl) { + throw new Error("No identity server base URL set"); + } + + let queryParams: QueryDict | undefined = undefined; + let body: Record | undefined = undefined; + if (method === Method.Get) { + queryParams = params; + } else { + body = params; + } + + const fullUri = this.getUrl(path, queryParams, prefix, this.opts.idBaseUrl); + + const opts: IRequestOpts = { + json: true, + headers: {}, + }; + if (accessToken) { + opts.headers.Authorization = `Bearer ${accessToken}`; + } + + return this.requestOtherUrl(method, fullUri, body, opts); + } + + /** + * Perform an authorised request to the homeserver. + * @param {string} method The HTTP method e.g. "GET". + * @param {string} path The HTTP path after the supplied prefix e.g. + * "/createRoom". + * + * @param {Object=} queryParams A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * + * @param {Object} [body] The HTTP JSON body. + * + * @param {Object|Number=} opts additional options. If a number is specified, + * this is treated as `opts.localTimeoutMs`. + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + * + * @param {string=} opts.prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + * + * @param {string=} opts.baseUrl The alternative base url to use. + * If not specified, uses this.opts.baseUrl + * + * @param {Object=} opts.headers map of additional request headers + * + * @return {Promise} Resolves to {data: {Object}, + * headers: {Object}, code: {Number}}. + * If onlyData is set, this will resolve to the data + * object only. + * @return {module:http-api.MatrixError} Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + */ + public authedRequest( + method: Method, + path: string, + queryParams?: QueryDict, + body?: Body, + opts: IRequestOpts = {}, + ): Promise> { + if (!queryParams) queryParams = {}; + + if (this.opts.useAuthorizationHeader) { + if (!opts.headers) { + opts.headers = {}; + } + if (!opts.headers.Authorization) { + opts.headers.Authorization = "Bearer " + this.opts.accessToken; + } + if (queryParams.access_token) { + delete queryParams.access_token; + } + } else if (!queryParams.access_token) { + queryParams.access_token = this.opts.accessToken; + } + + const requestPromise = this.request(method, path, queryParams, body, opts); + + requestPromise.catch((err: MatrixError) => { + if (err.errcode == 'M_UNKNOWN_TOKEN' && !opts?.inhibitLogoutEmit) { + this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err); + } else if (err.errcode == 'M_CONSENT_NOT_GIVEN') { + this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri); + } + }); + + // return the original promise, otherwise tests break due to it having to + // go around the event loop one more time to process the result of the request + return requestPromise; + } + + /** + * Perform a request to the homeserver without any credentials. + * @param {string} method The HTTP method e.g. "GET". + * @param {string} path The HTTP path after the supplied prefix e.g. + * "/createRoom". + * + * @param {Object=} queryParams A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * + * @param {Object} [body] The HTTP JSON body. + * + * @param {Object=} opts additional options + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + * + * @param {string=} opts.prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + * + * @param {Object=} opts.headers map of additional request headers + * + * @return {Promise} Resolves to {data: {Object}, + * headers: {Object}, code: {Number}}. + * If onlyData is set, this will resolve to the data + * object only. + * @return {module:http-api.MatrixError} Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + */ + public request( + method: Method, + path: string, + queryParams?: QueryDict, + body?: Body, + opts?: IRequestOpts, + ): Promise> { + const fullUri = this.getUrl(path, queryParams, opts?.prefix, opts?.baseUrl); + return this.requestOtherUrl(method, fullUri, body, opts); + } + + /** + * Perform a request to an arbitrary URL. + * @param {string} method The HTTP method e.g. "GET". + * @param {string} url The HTTP URL object. + * + * @param {Object} [body] The HTTP JSON body. + * + * @param {Object=} opts additional options + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + * + * @param {Object=} opts.headers map of additional request headers + * + * @return {Promise} Resolves to data unless `onlyData` is specified as false, + * where the resolved value will be a fetch Response object. + * @return {module:http-api.MatrixError} Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + */ + public async requestOtherUrl( + method: Method, + url: URL | string, + body?: Body, + opts: Pick = {}, + ): Promise> { + const headers = Object.assign({}, opts.headers || {}); + const json = opts.json ?? true; + // We can't use getPrototypeOf here as objects made in other contexts e.g. over postMessage won't have same ref + const jsonBody = json && body?.constructor?.name === Object.name; + + if (json) { + if (jsonBody && !headers["Content-Type"]) { + headers["Content-Type"] = "application/json"; + } + + if (!headers["Accept"]) { + headers["Accept"] = "application/json"; + } + } + + const timeout = opts.localTimeoutMs ?? this.opts.localTimeoutMs; + const signals = [ + this.abortController.signal, + ]; + if (timeout !== undefined) { + signals.push(timeoutSignal(timeout)); + } + if (opts.abortSignal) { + signals.push(opts.abortSignal); + } + + let data: BodyInit; + if (jsonBody) { + data = JSON.stringify(body); + } else { + data = body as BodyInit; + } + + const { signal, cleanup } = anySignal(signals); + + let res: Response; + try { + res = await this.fetch(url, { + signal, + method, + body: data, + headers, + mode: "cors", + redirect: "follow", + referrer: "", + referrerPolicy: "no-referrer", + cache: "no-cache", + credentials: "omit", // we send credentials via headers + }); + } catch (e) { + if (e.name === "AbortError") { + throw e; + } + throw new ConnectionError("fetch failed", e); + } finally { + cleanup(); + } + + if (!res.ok) { + throw parseErrorResponse(res, await res.text()); + } + + if (this.opts.onlyData) { + return json ? res.json() : res.text(); + } + return res as ResponseType; + } + + /** + * Form and return a homeserver request URL based on the given path params and prefix. + * @param {string} path The HTTP path after the supplied prefix e.g. "/createRoom". + * @param {Object} queryParams A dict of query params (these will NOT be urlencoded). + * @param {string} prefix The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix. + * @param {string} baseUrl The baseUrl to use e.g. "https://matrix.org/", defaulting to this.opts.baseUrl. + * @return {string} URL + */ + public getUrl( + path: string, + queryParams?: QueryDict, + prefix?: string, + baseUrl?: string, + ): URL { + const url = new URL((baseUrl ?? this.opts.baseUrl) + (prefix ?? this.opts.prefix) + path); + if (queryParams) { + utils.encodeParams(queryParams, url.searchParams); + } + return url; + } +} diff --git a/src/http-api/index.ts b/src/http-api/index.ts new file mode 100644 index 000000000..62e4b478e --- /dev/null +++ b/src/http-api/index.ts @@ -0,0 +1,216 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { FetchHttpApi } from "./fetch"; +import { FileType, IContentUri, IHttpOpts, Upload, UploadOpts, UploadResponse } from "./interface"; +import { MediaPrefix } from "./prefix"; +import * as utils from "../utils"; +import * as callbacks from "../realtime-callbacks"; +import { Method } from "./method"; +import { ConnectionError, MatrixError } from "./errors"; +import { parseErrorResponse } from "./utils"; + +export * from "./interface"; +export * from "./prefix"; +export * from "./errors"; +export * from "./method"; +export * from "./utils"; + +export class MatrixHttpApi extends FetchHttpApi { + private uploads: Upload[] = []; + + /** + * Upload content to the homeserver + * + * @param {object} file The object to upload. On a browser, something that + * can be sent to XMLHttpRequest.send (typically a File). Under node.js, + * a Buffer, String or ReadStream. + * + * @param {object} opts options object + * + * @param {string=} opts.name Name to give the file on the server. Defaults + * to file.name. + * + * @param {boolean=} opts.includeFilename if false will not send the filename, + * e.g for encrypted file uploads where filename leaks are undesirable. + * Defaults to true. + * + * @param {string=} opts.type Content-type for the upload. Defaults to + * file.type, or application/octet-stream. + * + * @param {boolean=} opts.rawResponse Return the raw body, rather than + * parsing the JSON. Defaults to false (except on node.js, where it + * defaults to true for backwards compatibility). + * + * @param {boolean=} opts.onlyContentUri Just return the content URI, + * rather than the whole body. Defaults to false (except on browsers, + * where it defaults to true for backwards compatibility). Ignored if + * opts.rawResponse is true. + * + * @param {Function=} opts.progressHandler Optional. Called when a chunk of + * data has been uploaded, with an object containing the fields `loaded` + * (number of bytes transferred) and `total` (total size, if known). + * + * @return {Promise} Resolves to response object, as + * determined by this.opts.onlyData, opts.rawResponse, and + * opts.onlyContentUri. Rejects with an error (usually a MatrixError). + */ + public uploadContent(file: FileType, opts: UploadOpts = {}): Promise { + const includeFilename = opts.includeFilename ?? true; + const abortController = opts.abortController ?? new AbortController(); + + // If the file doesn't have a mime type, use a default since the HS errors if we don't supply one. + const contentType = opts.type ?? (file as File).type ?? 'application/octet-stream'; + const fileName = opts.name ?? (file as File).name; + + const upload = { + loaded: 0, + total: 0, + abortController, + } as Upload; + const defer = utils.defer(); + + if (global.XMLHttpRequest) { + const xhr = new global.XMLHttpRequest(); + + const timeoutFn = function() { + xhr.abort(); + defer.reject(new Error("Timeout")); + }; + + // set an initial timeout of 30s; we'll advance it each time we get a progress notification + let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000); + + xhr.onreadystatechange = function() { + switch (xhr.readyState) { + case global.XMLHttpRequest.DONE: + callbacks.clearTimeout(timeoutTimer); + try { + if (xhr.status === 0) { + throw new DOMException(xhr.statusText, "AbortError"); // mimic fetch API + } + if (!xhr.responseText) { + throw new Error('No response body.'); + } + + if (xhr.status >= 400) { + defer.reject(parseErrorResponse(xhr, xhr.responseText)); + } else { + defer.resolve(JSON.parse(xhr.responseText)); + } + } catch (err) { + if (err.name === "AbortError") { + defer.reject(err); + return; + } + + (err).httpStatus = xhr.status; + defer.reject(new ConnectionError("request failed", err)); + } + break; + } + }; + + xhr.upload.onprogress = (ev: ProgressEvent) => { + callbacks.clearTimeout(timeoutTimer); + upload.loaded = ev.loaded; + upload.total = ev.total; + timeoutTimer = callbacks.setTimeout(timeoutFn, 30000); + opts.progressHandler?.({ + loaded: ev.loaded, + total: ev.total, + }); + }; + + const url = this.getUrl("/upload", undefined, MediaPrefix.R0); + + if (includeFilename && fileName) { + url.searchParams.set("filename", encodeURIComponent(fileName)); + } + + if (!this.opts.useAuthorizationHeader && this.opts.accessToken) { + url.searchParams.set("access_token", encodeURIComponent(this.opts.accessToken)); + } + + xhr.open(Method.Post, url.href); + if (this.opts.useAuthorizationHeader && this.opts.accessToken) { + xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken); + } + xhr.setRequestHeader("Content-Type", contentType); + xhr.send(file); + + abortController.signal.addEventListener("abort", () => { + xhr.abort(); + }); + } else { + const queryParams: utils.QueryDict = {}; + if (includeFilename && fileName) { + queryParams.filename = fileName; + } + + const headers: Record = { "Content-Type": contentType }; + + this.authedRequest( + Method.Post, "/upload", queryParams, file, { + prefix: MediaPrefix.R0, + headers, + abortSignal: abortController.signal, + }, + ).then(response => { + return this.opts.onlyData ? response : response.json(); + }).then(defer.resolve, defer.reject); + } + + // remove the upload from the list on completion + upload.promise = defer.promise.finally(() => { + utils.removeElement(this.uploads, elem => elem === upload); + }); + abortController.signal.addEventListener("abort", () => { + utils.removeElement(this.uploads, elem => elem === upload); + defer.reject(new DOMException("Aborted", "AbortError")); + }); + this.uploads.push(upload); + return upload.promise; + } + + public cancelUpload(promise: Promise): boolean { + const upload = this.uploads.find(u => u.promise === promise); + if (upload) { + upload.abortController.abort(); + return true; + } + return false; + } + + public getCurrentUploads(): Upload[] { + return this.uploads; + } + + /** + * Get the content repository url with query parameters. + * @return {Object} An object with a 'base', 'path' and 'params' for base URL, + * path and query parameters respectively. + */ + public getContentUri(): IContentUri { + return { + base: this.opts.baseUrl, + path: MediaPrefix.R0 + "/upload", + params: { + access_token: this.opts.accessToken, + }, + }; + } +} diff --git a/src/http-api/interface.ts b/src/http-api/interface.ts new file mode 100644 index 000000000..c798bec0d --- /dev/null +++ b/src/http-api/interface.ts @@ -0,0 +1,93 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixError } from "./errors"; + +export interface IHttpOpts { + fetchFn?: typeof global.fetch; + + baseUrl: string; + idBaseUrl?: string; + prefix: string; + extraParams?: Record; + + accessToken?: string; + useAuthorizationHeader?: boolean; // defaults to true + + onlyData?: boolean; + localTimeoutMs?: number; +} + +export interface IRequestOpts { + baseUrl?: string; + prefix?: string; + + headers?: Record; + abortSignal?: AbortSignal; + localTimeoutMs?: number; + json?: boolean; // defaults to true + + // Set to true to prevent the request function from emitting a Session.logged_out event. + // This is intended for use on endpoints where M_UNKNOWN_TOKEN is a valid/notable error response, + // such as with token refreshes. + inhibitLogoutEmit?: boolean; +} + +export interface IContentUri { + base: string; + path: string; + params: { + // eslint-disable-next-line camelcase + access_token: string; + }; +} + +export enum HttpApiEvent { + SessionLoggedOut = "Session.logged_out", + NoConsent = "no_consent", +} + +export type HttpApiEventHandlerMap = { + [HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void; + [HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void; +}; + +export interface UploadProgress { + loaded: number; + total: number; +} + +export interface UploadOpts { + name?: string; + type?: string; + includeFilename?: boolean; + progressHandler?(progress: UploadProgress): void; + abortController?: AbortController; +} + +export interface Upload { + loaded: number; + total: number; + promise: Promise; + abortController: AbortController; +} + +export interface UploadResponse { + // eslint-disable-next-line camelcase + content_uri: string; +} + +export type FileType = XMLHttpRequestBodyInit; diff --git a/src/http-api/method.ts b/src/http-api/method.ts new file mode 100644 index 000000000..1914360e3 --- /dev/null +++ b/src/http-api/method.ts @@ -0,0 +1,22 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export enum Method { + Get = "GET", + Put = "PUT", + Post = "POST", + Delete = "DELETE", +} diff --git a/src/http-api/prefix.ts b/src/http-api/prefix.ts new file mode 100644 index 000000000..8111bc355 --- /dev/null +++ b/src/http-api/prefix.ts @@ -0,0 +1,53 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export enum ClientPrefix { + /** + * A constant representing the URI path for release 0 of the Client-Server HTTP API. + */ + R0 = "/_matrix/client/r0", + /** + * A constant representing the URI path for the legacy release v1 of the Client-Server HTTP API. + */ + V1 = "/_matrix/client/v1", + /** + * A constant representing the URI path for Client-Server API endpoints versioned at v3. + */ + V3 = "/_matrix/client/v3", + /** + * A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs. + */ + Unstable = "/_matrix/client/unstable", +} + +export enum IdentityPrefix { + /** + * URI path for v1 of the identity API + * @deprecated Use v2. + */ + V1 = "/_matrix/identity/api/v1", + /** + * URI path for the v2 identity API + */ + V2 = "/_matrix/identity/api/v2", +} + +export enum MediaPrefix { + /** + * URI path for the media repo API + */ + R0 = "/_matrix/media/r0", +} diff --git a/src/http-api/utils.ts b/src/http-api/utils.ts new file mode 100644 index 000000000..220e0300b --- /dev/null +++ b/src/http-api/utils.ts @@ -0,0 +1,149 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { parse as parseContentType, ParsedMediaType } from "content-type"; + +import { logger } from "../logger"; +import { sleep } from "../utils"; +import { ConnectionError, MatrixError } from "./errors"; + +// Ponyfill for https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout +export function timeoutSignal(ms: number): AbortSignal { + const controller = new AbortController(); + setTimeout(() => { + controller.abort(); + }, ms); + + return controller.signal; +} + +export function anySignal(signals: AbortSignal[]): { + signal: AbortSignal; + cleanup(): void; +} { + const controller = new AbortController(); + + function cleanup() { + for (const signal of signals) { + signal.removeEventListener("abort", onAbort); + } + } + + function onAbort() { + controller.abort(); + cleanup(); + } + + for (const signal of signals) { + if (signal.aborted) { + onAbort(); + break; + } + signal.addEventListener("abort", onAbort); + } + + return { + signal: controller.signal, + cleanup, + }; +} + +/** + * Attempt to turn an HTTP error response into a Javascript Error. + * + * If it is a JSON response, we will parse it into a MatrixError. Otherwise + * we return a generic Error. + * + * @param {XMLHttpRequest|Response} response response object + * @param {String} body raw body of the response + * @returns {Error} + */ +export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error { + let contentType: ParsedMediaType; + try { + contentType = getResponseContentType(response); + } catch (e) { + return e; + } + + if (contentType?.type === "application/json" && body) { + return new MatrixError(JSON.parse(body), response.status); + } + if (contentType?.type === "text/plain") { + return new Error(`Server returned ${response.status} error: ${body}`); + } + return new Error(`Server returned ${response.status} error`); +} + +function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest { + return "getResponseHeader" in response; +} + +/** + * extract the Content-Type header from the response object, and + * parse it to a `{type, parameters}` object. + * + * returns null if no content-type header could be found. + * + * @param {XMLHttpRequest|Response} response response object + * @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found + */ +function getResponseContentType(response: XMLHttpRequest | Response): ParsedMediaType | null { + let contentType: string | null; + if (isXhr(response)) { + contentType = response.getResponseHeader("Content-Type"); + } else { + contentType = response.headers.get("Content-Type"); + } + + if (!contentType) return null; + + try { + return parseContentType(contentType); + } catch (e) { + throw new Error(`Error parsing Content-Type '${contentType}': ${e}`); + } +} + +/** + * Retries a network operation run in a callback. + * @param {number} maxAttempts maximum attempts to try + * @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again. + * @return {any} the result of the network operation + * @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError + */ +export async function retryNetworkOperation(maxAttempts: number, callback: () => Promise): Promise { + let attempts = 0; + let lastConnectionError: ConnectionError | null = null; + while (attempts < maxAttempts) { + try { + if (attempts > 0) { + const timeout = 1000 * Math.pow(2, attempts); + logger.log(`network operation failed ${attempts} times, retrying in ${timeout}ms...`); + await sleep(timeout); + } + return await callback(); + } catch (err) { + if (err instanceof ConnectionError) { + attempts += 1; + lastConnectionError = err; + } else { + throw err; + } + } + } + throw lastConnectionError; +} diff --git a/src/index.ts b/src/index.ts index c651438fb..4b8422435 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,17 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as request from "request"; - import * as matrixcs from "./matrix"; import * as utils from "./utils"; import { logger } from './logger'; -if (matrixcs.getRequest()) { +if (global.__js_sdk_entrypoint) { throw new Error("Multiple matrix-js-sdk entrypoints detected!"); } - -matrixcs.request(request); +global.__js_sdk_entrypoint = true; try { // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/src/matrix.ts b/src/matrix.ts index 6813655a9..eaa9b09e7 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -55,41 +55,6 @@ export { createNewMatrixCall, } from "./webrtc/call"; -// expose the underlying request object so different environments can use -// different request libs (e.g. request or browser-request) -let requestInstance; - -/** - * The function used to perform HTTP requests. Only use this if you want to - * use a different HTTP library, e.g. Angular's $http. This should - * be set prior to calling {@link createClient}. - * @param {requestFunction} r The request function to use. - */ -export function request(r) { - requestInstance = r; -} - -/** - * Return the currently-set request function. - * @return {requestFunction} The current request function. - */ -export function getRequest() { - return requestInstance; -} - -/** - * Apply wrapping code around the request function. The wrapper function is - * installed as the new request handler, and when invoked it is passed the - * previous value, along with the options and callback arguments. - * @param {requestWrapperFunction} wrapper The wrapping function. - */ -export function wrapRequest(wrapper) { - const origRequest = requestInstance; - requestInstance = function(options, callback) { - return wrapper(origRequest, options, callback); - }; -} - let cryptoStoreFactory = () => new MemoryCryptoStore; /** @@ -128,15 +93,13 @@ export interface ICryptoCallbacks { /** * Construct a Matrix Client. Similar to {@link module:client.MatrixClient} * except that the 'request', 'store' and 'scheduler' dependencies are satisfied. - * @param {(Object|string)} opts The configuration options for this client. If + * @param {(Object)} opts The configuration options for this client. If * this is a string, it is assumed to be the base URL. These configuration * options will be passed directly to {@link module:client.MatrixClient}. * @param {Object} opts.store If not set, defaults to * {@link module:store/memory.MemoryStore}. * @param {Object} opts.scheduler If not set, defaults to * {@link module:scheduler~MatrixScheduler}. - * @param {requestFunction} opts.request If not set, defaults to the function - * supplied to {@link request} which defaults to the request module from NPM. * * @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore * crypto store implementation. Calls the factory supplied to @@ -148,13 +111,7 @@ export interface ICryptoCallbacks { * @see {@link module:client.MatrixClient} for the full list of options for * opts. */ -export function createClient(opts: ICreateClientOpts | string) { - if (typeof opts === "string") { - opts = { - "baseUrl": opts, - }; - } - opts.request = opts.request || requestInstance; +export function createClient(opts: ICreateClientOpts) { opts.store = opts.store || new MemoryStore({ localStorage: global.localStorage, }); @@ -163,23 +120,6 @@ export function createClient(opts: ICreateClientOpts | string) { return new MatrixClient(opts); } -/** - * The request function interface for performing HTTP requests. This matches the - * API for the {@link https://github.com/request/request#requestoptions-callback| - * request NPM module}. The SDK will attempt to call this function in order to - * perform an HTTP request. - * @callback requestFunction - * @param {Object} opts The options for this HTTP request. - * @param {string} opts.uri The complete URI. - * @param {string} opts.method The HTTP method. - * @param {Object} opts.qs The query parameters to append to the URI. - * @param {Object} opts.body The JSON-serializable object. - * @param {boolean} opts.json True if this is a JSON request. - * @param {Object} opts._matrix_opts The underlying options set for - * {@link MatrixHttpApi}. - * @param {requestCallback} callback The request callback. - */ - /** * A wrapper for the request function interface. * @callback requestWrapperFunction diff --git a/src/models/MSC3089TreeSpace.ts b/src/models/MSC3089TreeSpace.ts index 0426d596e..9a9deec68 100644 --- a/src/models/MSC3089TreeSpace.ts +++ b/src/models/MSC3089TreeSpace.ts @@ -476,10 +476,8 @@ export class MSC3089TreeSpace { info: Partial, additionalContent?: IContent, ): Promise { - const mxc = await this.client.uploadContent(encryptedContents, { + const { content_uri: mxc } = await this.client.uploadContent(encryptedContents, { includeFilename: false, - onlyContentUri: true, - rawResponse: false, // make this explicit otherwise behaviour is different on browser vs NodeJS }); info.url = mxc; diff --git a/src/scheduler.ts b/src/scheduler.ts index 271982b74..2131e95c2 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -24,7 +24,7 @@ import { logger } from './logger'; import { MatrixEvent } from "./models/event"; import { EventType } from "./@types/event"; import { IDeferred } from "./utils"; -import { MatrixError } from "./http-api"; +import { ConnectionError, MatrixError } from "./http-api"; import { ISendEventResponse } from "./@types/requests"; const DEBUG = false; // set true to enable console logging. @@ -68,9 +68,7 @@ export class MatrixScheduler { // client error; no amount of retrying with save you now. return -1; } - // we ship with browser-request which returns { cors: rejected } when trying - // with no connection, so if we match that, give up since they have no conn. - if (err["cors"] === "rejected") { + if (err instanceof ConnectionError) { return -1; } diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 21d2af63f..e1072f8da 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -674,7 +674,7 @@ export class SlidingSyncSdk { member._requestedProfileInfo = true; // try to get a cached copy first. const user = client.getUser(member.userId); - let promise; + let promise: ReturnType; if (user) { promise = Promise.resolve({ avatar_url: user.avatarUrl, diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index da6419c96..5297ebd14 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -15,11 +15,11 @@ limitations under the License. */ import { logger } from './logger'; -import { IAbortablePromise } from "./@types/partials"; import { MatrixClient } from "./client"; import { IRoomEvent, IStateEvent } from "./sync-accumulator"; -import { TypedEventEmitter } from "./models//typed-event-emitter"; +import { TypedEventEmitter } from "./models/typed-event-emitter"; import { sleep, IDeferred, defer } from "./utils"; +import { ConnectionError } from "./http-api"; // /sync requests allow you to set a timeout= but the request may continue // beyond that and wedge forever, so we need to track how long we are willing @@ -353,7 +353,8 @@ export class SlidingSync extends TypedEventEmitter(); // the *desired* room subscriptions private confirmedRoomSubscriptions = new Set(); - private pendingReq?: IAbortablePromise; + private pendingReq?: Promise; + private abortController?: AbortController; /** * Create a new sliding sync instance @@ -700,7 +701,8 @@ export class SlidingSync extends TypedEventEmitter; + getOldestToDeviceBatch(): Promise; /** * Removes a specific batch of to-device messages from the queue diff --git a/src/store/indexeddb-backend.ts b/src/store/indexeddb-backend.ts index 3a14fed7d..f7547d6e5 100644 --- a/src/store/indexeddb-backend.ts +++ b/src/store/indexeddb-backend.ts @@ -33,7 +33,7 @@ export interface IIndexedDBBackend { getClientOptions(): Promise; storeClientOptions(options: IStartClientOpts): Promise; saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise; - getOldestToDeviceBatch(): Promise; + getOldestToDeviceBatch(): Promise; removeToDeviceBatch(id: number): Promise; } diff --git a/src/store/indexeddb-remote-backend.ts b/src/store/indexeddb-remote-backend.ts index 8be023f2b..d36404b90 100644 --- a/src/store/indexeddb-remote-backend.ts +++ b/src/store/indexeddb-remote-backend.ts @@ -138,7 +138,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { return this.doCmd('saveToDeviceBatches', [batches]); } - public async getOldestToDeviceBatch(): Promise { + public async getOldestToDeviceBatch(): Promise { return this.doCmd('getOldestToDeviceBatch'); } diff --git a/src/store/indexeddb.ts b/src/store/indexeddb.ts index 44f684bdf..2bc4f28f6 100644 --- a/src/store/indexeddb.ts +++ b/src/store/indexeddb.ts @@ -357,7 +357,7 @@ export class IndexedDBStore extends MemoryStore { return this.backend.saveToDeviceBatches(batches); } - public getOldestToDeviceBatch(): Promise { + public getOldestToDeviceBatch(): Promise { return this.backend.getOldestToDeviceBatch(); } diff --git a/src/sync.ts b/src/sync.ts index cee5e7f09..60fb34aed 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -57,7 +57,6 @@ import { RoomStateEvent, IMarkerFoundOptions } from "./models/room-state"; import { RoomMemberEvent } from "./models/room-member"; import { BeaconEvent } from "./models/beacon"; import { IEventsResponse } from "./@types/requests"; -import { IAbortablePromise } from "./@types/partials"; import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; import { Feature, ServerSupport } from "./feature"; @@ -164,7 +163,8 @@ type WrappedRoom = T & { */ export class SyncApi { private _peekRoom: Optional = null; - private currentSyncRequest: Optional> = null; + private currentSyncRequest: Optional> = null; + private abortController?: AbortController; private syncState: Optional = null; private syncStateData: Optional = null; // additional data (eg. error object for failed sync) private catchingUp = false; @@ -298,9 +298,9 @@ export class SyncApi { getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter, ).then(function(filterId) { qps.filter = filterId; - return client.http.authedRequest( - undefined, Method.Get, "/sync", qps as any, undefined, localTimeoutMs, - ); + return client.http.authedRequest(Method.Get, "/sync", qps as any, undefined, { + localTimeoutMs, + }); }).then(async (data) => { let leaveRooms = []; if (data.rooms?.leave) { @@ -433,11 +433,11 @@ export class SyncApi { } // FIXME: gut wrenching; hard-coded timeout values - this.client.http.authedRequest(undefined, Method.Get, "/events", { + this.client.http.authedRequest(Method.Get, "/events", { room_id: peekRoom.roomId, timeout: String(30 * 1000), from: token, - }, undefined, 50 * 1000).then((res) => { + }, undefined, { localTimeoutMs: 50 * 1000 }).then((res) => { if (this._peekRoom !== peekRoom) { debuglog("Stopped peeking in room %s", peekRoom.roomId); return; @@ -652,6 +652,7 @@ export class SyncApi { */ public async sync(): Promise { this.running = true; + this.abortController = new AbortController(); global.window?.addEventListener?.("online", this.onOnline, false); @@ -738,7 +739,7 @@ export class SyncApi { // but do not have global.window.removeEventListener. global.window?.removeEventListener?.("online", this.onOnline, false); this.running = false; - this.currentSyncRequest?.abort(); + this.abortController?.abort(); if (this.keepAliveTimer) { clearTimeout(this.keepAliveTimer); this.keepAliveTimer = null; @@ -902,12 +903,12 @@ export class SyncApi { } } - private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IAbortablePromise { + private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): Promise { const qps = this.getSyncParams(syncOptions, syncToken); - return this.client.http.authedRequest( - undefined, Method.Get, "/sync", qps as any, undefined, - qps.timeout + BUFFER_PERIOD_MS, - ); + return this.client.http.authedRequest(Method.Get, "/sync", qps as any, undefined, { + localTimeoutMs: qps.timeout + BUFFER_PERIOD_MS, + abortSignal: this.abortController?.signal, + }); } private getSyncParams(syncOptions: ISyncOptions, syncToken: string): ISyncParams { @@ -1521,7 +1522,6 @@ export class SyncApi { }; this.client.http.request( - undefined, // callback Method.Get, "/_matrix/client/versions", undefined, // queryParams undefined, // data diff --git a/src/utils.ts b/src/utils.ts index e4b8b466e..591803296 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -59,17 +59,23 @@ export function internaliseString(str: string): string { * {"foo": "bar", "baz": "taz"} * @return {string} The encoded string e.g. foo=bar&baz=taz */ -export function encodeParams(params: Record): string { - const searchParams = new URLSearchParams(); +export function encodeParams(params: QueryDict, urlSearchParams?: URLSearchParams): URLSearchParams { + const searchParams = urlSearchParams ?? new URLSearchParams(); for (const [key, val] of Object.entries(params)) { if (val !== undefined && val !== null) { - searchParams.set(key, String(val)); + if (Array.isArray(val)) { + val.forEach(v => { + searchParams.append(key, String(v)); + }); + } else { + searchParams.append(key, String(val)); + } } } - return searchParams.toString(); + return searchParams; } -export type QueryDict = Record; +export type QueryDict = Record; /** * Decode a query string in `application/x-www-form-urlencoded` format. @@ -80,8 +86,8 @@ export type QueryDict = Record; * This behaviour matches Node's qs.parse but is built on URLSearchParams * for native web compatibility */ -export function decodeParams(query: string): QueryDict { - const o: QueryDict = {}; +export function decodeParams(query: string): Record { + const o: Record = {}; const params = new URLSearchParams(query); for (const key of params.keys()) { const val = params.getAll(key); diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 90e20cc37..ad98e2079 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -298,7 +298,7 @@ export class MatrixCall extends TypedEventEmitter; + private inviteTimeout?: ReturnType; // The logic of when & if a call is on hold is nontrivial and explained in is*OnHold // This flag represents whether we want the other party to be on hold @@ -322,7 +322,7 @@ export class MatrixCall extends TypedEventEmitter; + private callLengthInterval?: ReturnType; private callLength = 0; constructor(opts: CallOpts) { @@ -1689,7 +1689,7 @@ export class MatrixCall extends TypedEventEmitter { - this.inviteTimeout = null; + this.inviteTimeout = undefined; if (this.state === CallState.InviteSent) { this.hangup(CallErrorCode.InviteTimeout, false); } @@ -2004,11 +2004,11 @@ export class MatrixCall extends TypedEventEmitter Date: Wed, 12 Oct 2022 18:07:58 +0000 Subject: [PATCH 40/48] Lock file maintenance (#2743) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 369 ++++++++++++++++++++++++++---------------------------- 1 file changed, 181 insertions(+), 188 deletions(-) diff --git a/yarn.lock b/yarn.lock index bb4d54802..36f97f31f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -58,10 +58,10 @@ dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8", "@babel/compat-data@^7.19.3": - version "7.19.3" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.3.tgz#707b939793f867f5a73b2666e6d9a3396eb03151" - integrity sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw== +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.19.3", "@babel/compat-data@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.4.tgz#95c86de137bf0317f3a570e1b6e996b427299747" + integrity sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw== "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.5": version "7.19.3" @@ -100,12 +100,12 @@ dependencies: eslint-rule-composer "^0.3.0" -"@babel/generator@^7.12.11", "@babel/generator@^7.19.3", "@babel/generator@^7.7.2": - version "7.19.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.3.tgz#d7f4d1300485b4547cb6f94b27d10d237b42bf59" - integrity sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ== +"@babel/generator@^7.12.11", "@babel/generator@^7.19.3", "@babel/generator@^7.19.4", "@babel/generator@^7.7.2": + version "7.19.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.5.tgz#da3f4b301c8086717eee9cab14da91b1fa5dcca7" + integrity sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg== dependencies: - "@babel/types" "^7.19.3" + "@babel/types" "^7.19.4" "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" @@ -256,11 +256,11 @@ "@babel/types" "^7.19.0" "@babel/helper-simple-access@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea" - integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g== + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz#be553f4951ac6352df2567f7daa19a0ee15668e7" + integrity sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg== dependencies: - "@babel/types" "^7.18.6" + "@babel/types" "^7.19.4" "@babel/helper-skip-transparent-expression-wrappers@^7.18.9": version "7.18.9" @@ -276,10 +276,10 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-string-parser@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" - integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" @@ -302,13 +302,13 @@ "@babel/types" "^7.19.0" "@babel/helpers@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.19.0.tgz#f30534657faf246ae96551d88dd31e9d1fa1fc18" - integrity sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg== + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.19.4.tgz#42154945f87b8148df7203a25c31ba9a73be46c5" + integrity sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw== dependencies: "@babel/template" "^7.18.10" - "@babel/traverse" "^7.19.0" - "@babel/types" "^7.19.0" + "@babel/traverse" "^7.19.4" + "@babel/types" "^7.19.4" "@babel/highlight@^7.18.6": version "7.18.6" @@ -319,10 +319,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.19.3", "@babel/parser@^7.2.3", "@babel/parser@^7.9.4": - version "7.19.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.3.tgz#8dd36d17c53ff347f9e55c328710321b49479a9a" - integrity sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.19.3", "@babel/parser@^7.19.4", "@babel/parser@^7.2.3", "@babel/parser@^7.9.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.4.tgz#03c4339d2b8971eb3beca5252bafd9b9f79db3dc" + integrity sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -415,14 +415,14 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz#f9434f6beb2c8cae9dfcf97d2a5941bbbf9ad4e7" - integrity sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q== +"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.19.4.tgz#a8fc86e8180ff57290c91a75d83fe658189b642d" + integrity sha512-wHmj6LDxVDnL+3WhXteUBaoM1aVILZODAUjg11kHqG4cOlfgMQGxw6aCgvrXrmaJR3Bn14oZhImyCPZzRpC93Q== dependencies: - "@babel/compat-data" "^7.18.8" - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/compat-data" "^7.19.4" + "@babel/helper-compilation-targets" "^7.19.3" + "@babel/helper-plugin-utils" "^7.19.0" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.18.8" @@ -625,12 +625,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-block-scoping@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.9.tgz#f9b7e018ac3f373c81452d6ada8bd5a18928926d" - integrity sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw== +"@babel/plugin-transform-block-scoping@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.19.4.tgz#315d70f68ce64426db379a3d830e7ac30be02e9b" + integrity sha512-934S2VLLlt2hRJwPf4MczaOr4hYF0z+VKPwqTNxyKX7NthTiPfhuKFWQZHXRM0vh/wo/VyXB3s4bZUNA08l+tQ== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.19.0" "@babel/plugin-transform-classes@^7.19.0": version "7.19.0" @@ -654,12 +654,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.9" -"@babel/plugin-transform-destructuring@^7.18.13": - version "7.18.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.13.tgz#9e03bc4a94475d62b7f4114938e6c5c33372cbf5" - integrity sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow== +"@babel/plugin-transform-destructuring@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.19.4.tgz#46890722687b9b89e1369ad0bd8dc6c5a3b4319d" + integrity sha512-t0j0Hgidqf0aM86dF8U+vXYReUgJnlv4bZLsyoPnwZNrGY+7/38o8YjaELrvHeVfTZao15kjR0PVv0nju2iduA== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.19.0" "@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.18.6" @@ -877,11 +877,11 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/preset-env@^7.12.11": - version "7.19.3" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.19.3.tgz#52cd19abaecb3f176a4ff9cc5e15b7bf06bec754" - integrity sha512-ziye1OTc9dGFOAXSWKUqQblYHNlBOaDl8wzqf2iKXJAltYiR3hKHUKmkt+S9PppW7RQpq4fFCrwwpIDj/f5P4w== + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.19.4.tgz#4c91ce2e1f994f717efb4237891c3ad2d808c94b" + integrity sha512-5QVOTXUdqTCjQuh2GGtdd7YEhoRXBMVGROAtsBeLGIbIz3obCBIfRMT1I3ZKkMgNzwkyCkftDXSSkHxnfVf4qg== dependencies: - "@babel/compat-data" "^7.19.3" + "@babel/compat-data" "^7.19.4" "@babel/helper-compilation-targets" "^7.19.3" "@babel/helper-plugin-utils" "^7.19.0" "@babel/helper-validator-option" "^7.18.6" @@ -896,7 +896,7 @@ "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" "@babel/plugin-proposal-numeric-separator" "^7.18.6" - "@babel/plugin-proposal-object-rest-spread" "^7.18.9" + "@babel/plugin-proposal-object-rest-spread" "^7.19.4" "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" "@babel/plugin-proposal-optional-chaining" "^7.18.9" "@babel/plugin-proposal-private-methods" "^7.18.6" @@ -920,10 +920,10 @@ "@babel/plugin-transform-arrow-functions" "^7.18.6" "@babel/plugin-transform-async-to-generator" "^7.18.6" "@babel/plugin-transform-block-scoped-functions" "^7.18.6" - "@babel/plugin-transform-block-scoping" "^7.18.9" + "@babel/plugin-transform-block-scoping" "^7.19.4" "@babel/plugin-transform-classes" "^7.19.0" "@babel/plugin-transform-computed-properties" "^7.18.9" - "@babel/plugin-transform-destructuring" "^7.18.13" + "@babel/plugin-transform-destructuring" "^7.19.4" "@babel/plugin-transform-dotall-regex" "^7.18.6" "@babel/plugin-transform-duplicate-keys" "^7.18.9" "@babel/plugin-transform-exponentiation-operator" "^7.18.6" @@ -950,7 +950,7 @@ "@babel/plugin-transform-unicode-escapes" "^7.18.10" "@babel/plugin-transform-unicode-regex" "^7.18.6" "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.19.3" + "@babel/types" "^7.19.4" babel-plugin-polyfill-corejs2 "^0.3.3" babel-plugin-polyfill-corejs3 "^0.6.0" babel-plugin-polyfill-regenerator "^0.4.1" @@ -989,9 +989,9 @@ source-map-support "^0.5.16" "@babel/runtime@^7.12.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" - integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78" + integrity sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA== dependencies: regenerator-runtime "^0.13.4" @@ -1004,28 +1004,28 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@^7.1.6", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.19.3", "@babel/traverse@^7.7.2": - version "7.19.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.3.tgz#3a3c5348d4988ba60884e8494b0592b2f15a04b4" - integrity sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ== +"@babel/traverse@^7.1.6", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.19.3", "@babel/traverse@^7.19.4", "@babel/traverse@^7.7.2": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.4.tgz#f117820e18b1e59448a6c1fa9d0ff08f7ac459a8" + integrity sha512-w3K1i+V5u2aJUOXBFFC5pveFLmtq1s3qcdDNC2qRI6WPBQIDaKFqXxDEqDO/h1dQ3HjsZoZMyIy6jGLq0xtw+g== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.19.3" + "@babel/generator" "^7.19.4" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.19.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.19.3" - "@babel/types" "^7.19.3" + "@babel/parser" "^7.19.4" + "@babel/types" "^7.19.4" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.19.3", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.19.3" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.3.tgz#fc420e6bbe54880bce6779ffaf315f5e43ec9624" - integrity sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw== +"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.19.3", "@babel/types@^7.19.4", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.4.tgz#0dd5c91c573a202d600490a35b33246fed8a41c7" + integrity sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw== dependencies: - "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-string-parser" "^7.19.4" "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" @@ -1035,9 +1035,9 @@ integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== "@eslint/eslintrc@^1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.2.tgz#58b69582f3b7271d8fa67fe5251767a5b38ea356" - integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ== + version "1.3.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95" + integrity sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -1336,7 +1336,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@^3.0.3": +"@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== @@ -1354,21 +1354,22 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@^1.4.10": +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.8", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.15" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" - integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== + version "0.3.16" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz#a7982f16c18cae02be36274365433e5b49d7b23f" + integrity sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA== dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz": version "3.2.13" + uid "0109fde93bcc61def851f79826c9384c073b5175" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz#0109fde93bcc61def851f79826c9384c073b5175" "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": @@ -1518,9 +1519,9 @@ tslib "^2.4.0" "@sinclair/typebox@^0.24.1": - version "0.24.44" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.44.tgz#0a0aa3bf4a155a678418527342a3ee84bd8caa5c" - integrity sha512-ka0W0KN5i6LfrSocduwliMMpqVgohtPFidKdMEOUjoOFCHcOOYkKsPRxfs5f15oPNHTm6ERAm0GV/+/LTKeiWg== + version "0.24.46" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.46.tgz#57501b58023776dbbae9e25619146286440be34c" + integrity sha512-ng4ut1z2MCBhK/NwDVwIQp3pAUOCs/KNaW3cBxdFB2xTDrOuo1xuNmpr/9HHFhxqIvHrs1NTH3KJg6q+JSy1Kw== "@sinonjs/commons@^1.7.0": version "1.8.3" @@ -1627,9 +1628,9 @@ "@types/istanbul-lib-report" "*" "@types/jest@^29.0.0": - version "29.1.1" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.1.1.tgz#cf21a0835a1ba9a30ea1966019f1261c6a114c92" - integrity sha512-U9Ey07dGWl6fUFaIaUQUKWG5NoKi/zizeVQCGV8s4nSU0jPgqphVZvS64+8BtWYvrc3ZGw6wo943NSYPxkrp/g== + version "29.1.2" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.1.2.tgz#7ad8077043ab5f6c108c8111bcc1d224e5600a87" + integrity sha512-y+nlX0h87U0R+wsGn6EBuoRWYyv3KFtwRNP3QWp9+k2tJ2/bqcGS3UxD7jgT+tiwJWWq3UsyV4Y+T6rsMT4XMg== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -1663,14 +1664,14 @@ integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== "@types/node@*": - version "18.8.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.2.tgz#17d42c6322d917764dd3d2d3a10d7884925de067" - integrity sha512-cRMwIgdDN43GO4xMWAfJAecYn8wV4JbsOGHNfNUIDiuYkUYAR5ec4Rj7IO2SAhFPEfpPtLtUTbbny/TCT7aDwA== + version "18.8.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.4.tgz#54be907698f40de8a45770b48486aa3cbd3adff7" + integrity sha512-WdlVphvfR/GJCLEMbNA8lJ0lhFNBj4SW3O+O5/cEGw9oYrv0al9zTwuQsq+myDUXgNx2jgBynoVgZ2MMJ6pbow== "@types/node@16": - version "16.11.64" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.64.tgz#9171f327298b619e2c52238b120c19056415d820" - integrity sha512-z5hPTlVFzNwtJ2LNozTpJcD1Cu44c4LNuzaq1mwxmiHWQh2ULdR6Vjwo1UGldzRpzL0yUEdZddnfqGW2G70z6Q== + version "16.11.65" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.65.tgz#59500b86af757d6fcabd3dec32fecb6e357d7a45" + integrity sha512-Vfz7wGMOr4jbQGiQHVJm8VjeQwM9Ya7mHe9LtQ264/Epf5n1KiZShOFqk++nBzw6a/ubgYdB9Od7P+MH/LjoWw== "@types/prettier@^2.1.5": version "2.7.1" @@ -1712,13 +1713,13 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.6.0": - version "5.39.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.39.0.tgz#778b2d9e7f293502c7feeea6c74dca8eb3e67511" - integrity sha512-xVfKOkBm5iWMNGKQ2fwX5GVgBuHmZBO1tCRwXmY5oAIsPscfwm2UADDuNB8ZVYCtpQvJK4xpjrK7jEhcJ0zY9A== + version "5.40.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.0.tgz#0159bb71410eec563968288a17bd4478cdb685bd" + integrity sha512-FIBZgS3DVJgqPwJzvZTuH4HNsZhHMa9SjxTKAZTlMsPw/UzpEjcf9f4dfgDJEHjK+HboUJo123Eshl6niwEm/Q== dependencies: - "@typescript-eslint/scope-manager" "5.39.0" - "@typescript-eslint/type-utils" "5.39.0" - "@typescript-eslint/utils" "5.39.0" + "@typescript-eslint/scope-manager" "5.40.0" + "@typescript-eslint/type-utils" "5.40.0" + "@typescript-eslint/utils" "5.40.0" debug "^4.3.4" ignore "^5.2.0" regexpp "^3.2.0" @@ -1726,69 +1727,70 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.6.0": - version "5.39.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.39.0.tgz#93fa0bc980a3a501e081824f6097f7ca30aaa22b" - integrity sha512-PhxLjrZnHShe431sBAGHaNe6BDdxAASDySgsBCGxcBecVCi8NQWxQZMcizNA4g0pN51bBAn/FUfkWG3SDVcGlA== + version "5.40.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.40.0.tgz#432bddc1fe9154945660f67c1ba6d44de5014840" + integrity sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw== dependencies: - "@typescript-eslint/scope-manager" "5.39.0" - "@typescript-eslint/types" "5.39.0" - "@typescript-eslint/typescript-estree" "5.39.0" + "@typescript-eslint/scope-manager" "5.40.0" + "@typescript-eslint/types" "5.40.0" + "@typescript-eslint/typescript-estree" "5.40.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.39.0": - version "5.39.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.39.0.tgz#873e1465afa3d6c78d8ed2da68aed266a08008d0" - integrity sha512-/I13vAqmG3dyqMVSZPjsbuNQlYS082Y7OMkwhCfLXYsmlI0ca4nkL7wJ/4gjX70LD4P8Hnw1JywUVVAwepURBw== +"@typescript-eslint/scope-manager@5.40.0": + version "5.40.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.40.0.tgz#d6ea782c8e3a2371ba3ea31458dcbdc934668fc4" + integrity sha512-d3nPmjUeZtEWRvyReMI4I1MwPGC63E8pDoHy0BnrYjnJgilBD3hv7XOiETKLY/zTwI7kCnBDf2vWTRUVpYw0Uw== dependencies: - "@typescript-eslint/types" "5.39.0" - "@typescript-eslint/visitor-keys" "5.39.0" + "@typescript-eslint/types" "5.40.0" + "@typescript-eslint/visitor-keys" "5.40.0" -"@typescript-eslint/type-utils@5.39.0": - version "5.39.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.39.0.tgz#0a8c00f95dce4335832ad2dc6bc431c14e32a0a6" - integrity sha512-KJHJkOothljQWzR3t/GunL0TPKY+fGJtnpl+pX+sJ0YiKTz3q2Zr87SGTmFqsCMFrLt5E0+o+S6eQY0FAXj9uA== +"@typescript-eslint/type-utils@5.40.0": + version "5.40.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.40.0.tgz#4964099d0158355e72d67a370249d7fc03331126" + integrity sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw== dependencies: - "@typescript-eslint/typescript-estree" "5.39.0" - "@typescript-eslint/utils" "5.39.0" + "@typescript-eslint/typescript-estree" "5.40.0" + "@typescript-eslint/utils" "5.40.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.39.0": - version "5.39.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.39.0.tgz#f4e9f207ebb4579fd854b25c0bf64433bb5ed78d" - integrity sha512-gQMZrnfEBFXK38hYqt8Lkwt8f4U6yq+2H5VDSgP/qiTzC8Nw8JO3OuSUOQ2qW37S/dlwdkHDntkZM6SQhKyPhw== +"@typescript-eslint/types@5.40.0": + version "5.40.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.40.0.tgz#8de07e118a10b8f63c99e174a3860f75608c822e" + integrity sha512-V1KdQRTXsYpf1Y1fXCeZ+uhjW48Niiw0VGt4V8yzuaDTU8Z1Xl7yQDyQNqyAFcVhpYXIVCEuxSIWTsLDpHgTbw== -"@typescript-eslint/typescript-estree@5.39.0": - version "5.39.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.39.0.tgz#c0316aa04a1a1f4f7f9498e3c13ef1d3dc4cf88b" - integrity sha512-qLFQP0f398sdnogJoLtd43pUgB18Q50QSA+BTE5h3sUxySzbWDpTSdgt4UyxNSozY/oDK2ta6HVAzvGgq8JYnA== +"@typescript-eslint/typescript-estree@5.40.0": + version "5.40.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.0.tgz#e305e6a5d65226efa5471ee0f12e0ffaab6d3075" + integrity sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg== dependencies: - "@typescript-eslint/types" "5.39.0" - "@typescript-eslint/visitor-keys" "5.39.0" + "@typescript-eslint/types" "5.40.0" + "@typescript-eslint/visitor-keys" "5.40.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.39.0": - version "5.39.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.39.0.tgz#b7063cca1dcf08d1d21b0d91db491161ad0be110" - integrity sha512-+DnY5jkpOpgj+EBtYPyHRjXampJfC0yUZZzfzLuUWVZvCuKqSdJVC8UhdWipIw7VKNTfwfAPiOWzYkAwuIhiAg== +"@typescript-eslint/utils@5.40.0": + version "5.40.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.40.0.tgz#647f56a875fd09d33c6abd70913c3dd50759b772" + integrity sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.39.0" - "@typescript-eslint/types" "5.39.0" - "@typescript-eslint/typescript-estree" "5.39.0" + "@typescript-eslint/scope-manager" "5.40.0" + "@typescript-eslint/types" "5.40.0" + "@typescript-eslint/typescript-estree" "5.40.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" + semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.39.0": - version "5.39.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.39.0.tgz#8f41f7d241b47257b081ddba5d3ce80deaae61e2" - integrity sha512-yyE3RPwOG+XJBLrhvsxAidUgybJVQ/hG8BhiJo0k8JSAYfk/CshVcxf0HwP4Jt7WZZ6vLmxdo1p6EyN3tzFTkg== +"@typescript-eslint/visitor-keys@5.40.0": + version "5.40.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.0.tgz#dd2d38097f68e0d2e1e06cb9f73c0173aca54b68" + integrity sha512-ijJ+6yig+x9XplEpG2K6FUdJeQGGj/15U3S56W9IqXKJqleuD7zJ2AX/miLezwxpd7ZxDAqO87zWufKg+RPZyQ== dependencies: - "@typescript-eslint/types" "5.39.0" + "@typescript-eslint/types" "5.40.0" eslint-visitor-keys "^3.3.0" JSONStream@^1.0.3: @@ -2157,9 +2159,9 @@ base64-js@^1.0.2: integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== before-after-hook@^2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" - integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== + version "2.2.3" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" + integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== better-docs@^2.4.0-beta.9: version "2.7.2" @@ -2453,9 +2455,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001400: - version "1.0.30001414" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001414.tgz#5f1715e506e71860b4b07c50060ea6462217611e" - integrity sha512-t55jfSaWjCdocnFdKQoO+d2ct9C59UZg4dY3OnUlSZ447r8pUtIKdp0hpAzrGFultmTC+Us+KpKi4GZl/LXlFg== + version "1.0.30001418" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz#5f459215192a024c99e3e3a53aac310fc7cf24e6" + integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg== catharsis@^0.9.0: version "0.9.0" @@ -2517,9 +2519,9 @@ chokidar@^3.4.0: fsevents "~2.3.2" ci-info@^3.2.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.4.0.tgz#b28484fd436cbc267900364f096c9dc185efb251" - integrity sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug== + version "3.5.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.5.0.tgz#bfac2a29263de4c829d806b1ab478e35091e171f" + integrity sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw== cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" @@ -2688,11 +2690,9 @@ content-type@^1.0.4: integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" - integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== - dependencies: - safe-buffer "~5.1.1" + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== convert-source-map@~1.1.0: version "1.1.3" @@ -2700,9 +2700,9 @@ convert-source-map@~1.1.0: integrity sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg== core-js-compat@^3.25.1: - version "3.25.4" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.25.4.tgz#730a255d4a47a937513abf1672bf278dc24dcebf" - integrity sha512-gCEcIEEqCR6230WroNunK/653CWKhqyCKJ9b+uESqOt/WFJA8B4lTnnQFdpYY5vmBcwJAA90Bo5vXs+CVsf6iA== + version "3.25.5" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.25.5.tgz#0016e8158c904f7b059486639e6e82116eafa7d9" + integrity sha512-ovcyhs2DEBUIE0MGEKHP4olCUW/XYte3Vroyxuh38rD1wAO4dHohsovUC4eAOuzFxE6b+RXvBU3UZ9o0YhUTkA== dependencies: browserslist "^4.21.4" @@ -2846,9 +2846,9 @@ define-properties@^1.1.3, define-properties@^1.1.4: object-keys "^1.1.1" defined@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" - integrity sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ== + version "1.0.1" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.1.tgz#c0b9db27bfaffd95d6f61399419b893df0f91ebf" + integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q== deprecation@^2.0.0, deprecation@^2.3.1: version "2.3.1" @@ -2969,9 +2969,9 @@ duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: readable-stream "^2.0.2" electron-to-chromium@^1.4.251: - version "1.4.270" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.270.tgz#2c6ea409b45cdb5c3e0cb2c08cf6c0ba7e0f2c26" - integrity sha512-KNhIzgLiJmDDC444dj9vEOpZEgsV96ult9Iff98Vanumn+ShJHd5se8aX6KeVxdc0YQeqdrezBZv89rleDbvSg== + version "1.4.279" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.279.tgz#84267fec806a8b1c5a1daebf726c4e296e5bcdf9" + integrity sha512-xs7vEuSZ84+JsHSTFqqG0TE3i8EAivHomRQZhhcRvsmnjsh5C2KdhwNKf4ZRYtzq75wojpFyqb62m32Oam57wA== elliptic@^6.5.3: version "6.5.4" @@ -3017,9 +3017,9 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.0: - version "1.20.3" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.3.tgz#90b143ff7aedc8b3d189bcfac7f1e3e3f81e9da1" - integrity sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw== + version "1.20.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.4.tgz#1d103f9f8d78d4cf0713edcd6d0ed1a46eed5861" + integrity sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" @@ -3031,7 +3031,7 @@ es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19 has-property-descriptors "^1.0.0" has-symbols "^1.0.3" internal-slot "^1.0.3" - is-callable "^1.2.6" + is-callable "^1.2.7" is-negative-zero "^2.0.2" is-regex "^1.1.4" is-shared-array-buffer "^1.0.2" @@ -3897,7 +3897,7 @@ is-buffer@^1.1.0, is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.6: +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== @@ -4077,9 +4077,9 @@ istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz#31d18bdd127f825dd02ea7bfdfd906f8ab840e9f" - integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A== + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== dependencies: "@babel/core" "^7.12.3" "@babel/parser" "^7.14.7" @@ -4856,9 +4856,9 @@ matrix-events-sdk@^0.0.1-beta.7: integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== matrix-mock-request@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.5.0.tgz#78da2590e82be2e31edcf9814833af5e5f8d2f1a" - integrity sha512-7T3gklpW+4rfHsTnp/FDML7aWoBrXhAh8+1ltinQfAh9TDj6y382z/RUMR7i03d1WDzt/ed1UTihqO5GDoOq9Q== + version "2.6.0" + resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.6.0.tgz#0855c10b250668ce542b697251087be2bcc23f92" + integrity sha512-D0n+FsoMvHBrBoo60IeGhyrNoCBdT8n+Wl+LMW+k5aR+k9QAxqGopPzJNk1tqeaJLFUhmvYLuNc8/VBKRpPy+Q== dependencies: expect "^28.1.0" @@ -4944,9 +4944,9 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: brace-expansion "^1.1.7" minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== mkdirp-classic@^0.5.2: version "0.5.3" @@ -5829,14 +5829,7 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.5: - version "7.3.7" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.7: +semver@^7.3.5, semver@^7.3.7: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== @@ -6163,9 +6156,9 @@ terminal-link@^2.0.0: supports-hyperlinks "^2.0.0" terser@^5.5.1: - version "5.15.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.0.tgz#e16967894eeba6e1091509ec83f0c60e179f2425" - integrity sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA== + version "5.15.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.1.tgz#8561af6e0fd6d839669c73b92bdd5777d870ed6c" + integrity sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw== dependencies: "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" @@ -6476,9 +6469,9 @@ universal-user-agent@^6.0.0: integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== update-browserslist-db@^1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz#2924d3927367a38d5c555413a7ce138fc95fcb18" - integrity sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg== + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -6563,9 +6556,9 @@ vue-docgen-api@^3.26.0: vue-template-compiler "^2.0.0" vue-template-compiler@^2.0.0: - version "2.7.10" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.10.tgz#9e20f35b2fdccacacf732dd7dedb49bf65f4556b" - integrity sha512-QO+8R9YRq1Gudm8ZMdo/lImZLJVUIAM8c07Vp84ojdDAf8HmPJc7XB556PcXV218k2AkKznsRz6xB5uOjAC4EQ== + version "2.7.12" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.12.tgz#762476971b13eb5ea5c786886be28395f345c1e1" + integrity sha512-6rhJAuo2vRzJMs8X/pd9yqtsJmnPEnv4E0cb9KCu0sfGhoDt8roCCa/6qbrvpc1b38zYgdmY/xrk4qfNWZIjwA== dependencies: de-indent "^1.0.2" he "^1.2.0" From cc025ea45844347320a625edd2acfff641f74125 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Oct 2022 14:10:47 -0400 Subject: [PATCH 41/48] Fix more key backup paths being unstable (#2746) --- src/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index d8392ae92..572ae1d37 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2924,7 +2924,7 @@ export class MatrixClient extends TypedEventEmitter( Method.Get, path.path, path.queryData, undefined, - { prefix: ClientPrefix.Unstable }, + { prefix: ClientPrefix.V3 }, ); if ((res as IRoomsKeysResponse).rooms) { @@ -3276,7 +3276,7 @@ export class MatrixClient extends TypedEventEmitter Date: Thu, 13 Oct 2022 09:07:15 +0100 Subject: [PATCH 42/48] Improve MatrixError message (#2749) --- spec/integ/matrix-client-methods.spec.ts | 2 +- src/http-api/errors.ts | 12 +++++++++--- src/http-api/utils.ts | 6 +++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index 5bec405cb..954df27fd 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -121,7 +121,7 @@ describe("MatrixClient", function() { }, function(error) { expect(error.httpStatus).toEqual(400); expect(error.errcode).toEqual("M_SNAFU"); - expect(error.message).toEqual("broken"); + expect(error.message).toEqual("MatrixError: [400] broken"); }); httpBackend!.flush(''); diff --git a/src/http-api/errors.ts b/src/http-api/errors.ts index d44b23e7a..cf057aff7 100644 --- a/src/http-api/errors.ts +++ b/src/http-api/errors.ts @@ -37,11 +37,17 @@ export class MatrixError extends Error { public readonly errcode?: string; public readonly data: IErrorJson; - constructor(errorJson: IErrorJson = {}, public httpStatus?: number) { - super(`MatrixError: ${errorJson.errcode}`); + constructor(errorJson: IErrorJson = {}, public httpStatus?: number, public url?: string) { + let message = errorJson.error || "Unknown message"; + if (httpStatus) { + message = `[${httpStatus}] ${message}`; + } + if (url) { + message = `${message} (${url})`; + } + super(`MatrixError: ${message}`); this.errcode = errorJson.errcode; this.name = errorJson.errcode || "Unknown error code"; - this.message = errorJson.error || "Unknown message"; this.data = errorJson; } } diff --git a/src/http-api/utils.ts b/src/http-api/utils.ts index 220e0300b..ac0eafb4b 100644 --- a/src/http-api/utils.ts +++ b/src/http-api/utils.ts @@ -80,7 +80,11 @@ export function parseErrorResponse(response: XMLHttpRequest | Response, body?: s } if (contentType?.type === "application/json" && body) { - return new MatrixError(JSON.parse(body), response.status); + return new MatrixError( + JSON.parse(body), + response.status, + isXhr(response) ? response.responseURL : response.url, + ); } if (contentType?.type === "text/plain") { return new Error(`Server returned ${response.status} error: ${body}`); From bb3d51652df6b28f9298afc990010c76886e2d99 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 13 Oct 2022 10:52:40 +0100 Subject: [PATCH 43/48] Consolidate error handling retry logic --- src/sliding-sync.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 235f8ad40..9479991aa 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -846,17 +846,15 @@ export class SlidingSync extends TypedEventEmitter Date: Thu, 13 Oct 2022 11:07:34 +0100 Subject: [PATCH 44/48] New style abort --- src/sliding-sync.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 5bf7a5007..2c42751a0 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -763,7 +763,8 @@ export class SlidingSync extends TypedEventEmitter(); // leave desired ones alone though! // reset the connection as we might be wedged this.needsResend = true; - this.pendingReq?.abort(); + this.abortController?.abort(); + this.abortController = new AbortController(); } /** From f81e53c90887a6b04da63822f2351be8c04dca50 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 13 Oct 2022 11:17:44 +0100 Subject: [PATCH 45/48] Rejig timeout function to cancel the timer when listenUntil returns --- spec/integ/sliding-sync.spec.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index f7ec7c747..19c6cf88a 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -1186,9 +1186,20 @@ describe("SlidingSync", () => { }); }); -async function timeout(delayMs: number, reason: string): Promise { - await sleep(delayMs); - throw new Error(`timeout: ${delayMs}ms - ${reason}`); +function timeout(delayMs: number, reason: string): { promise: Promise, cancel: () => void } { + let timeoutId; + return { + promise: new Promise((resolve, reject) => { + timeoutId = setTimeout(() => { + reject(new Error(`timeout: ${delayMs}ms - ${reason}`)); + }, delayMs); + }), + cancel: () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + } + } } /** @@ -1207,18 +1218,21 @@ function listenUntil( timeoutMs = 500, ): Promise { const trace = new Error().stack?.split(`\n`)[2]; + const t = timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace); return Promise.race([new Promise((resolve, reject) => { const wrapper = (...args) => { try { const data = callback(...args); if (data) { emitter.off(eventName, wrapper); + t.cancel(); resolve(data); } } catch (err) { reject(err); + t.cancel(); } }; emitter.on(eventName, wrapper); - }), timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace)]); + }), t.promise]); } From 007ca977415d379d7a3eef9ba225bb0120d23da5 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 13 Oct 2022 11:21:27 +0100 Subject: [PATCH 46/48] Linting --- spec/integ/sliding-sync.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 19c6cf88a..3390b48be 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -22,7 +22,6 @@ import { SlidingSync, SlidingSyncState, ExtensionState, SlidingSyncEvent } from import { TestClient } from "../TestClient"; import { logger } from "../../src/logger"; import { MatrixClient } from "../../src"; -import { sleep } from "../../src/utils"; /** * Tests for sliding sync. These tests are broken down into sub-tests which are reliant upon one another. @@ -1198,8 +1197,8 @@ function timeout(delayMs: number, reason: string): { promise: Promise, ca if (timeoutId) { clearTimeout(timeoutId); } - } - } + }, + }; } /** From 50dd79c5953f3c34db162572dca6042a5175ffd2 Mon Sep 17 00:00:00 2001 From: kegsay Date: Thu, 13 Oct 2022 13:18:31 +0100 Subject: [PATCH 47/48] Check for AbortError, not any generic connection error, to avoid tightlooping (#2752) --- src/sliding-sync.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 2c42751a0..480ee818c 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -19,7 +19,6 @@ import { MatrixClient } from "./client"; import { IRoomEvent, IStateEvent } from "./sync-accumulator"; import { TypedEventEmitter } from "./models/typed-event-emitter"; import { sleep, IDeferred, defer } from "./utils"; -import { ConnectionError } from "./http-api"; // /sync requests allow you to set a timeout= but the request may continue // beyond that and wedge forever, so we need to track how long we are willing @@ -852,7 +851,7 @@ export class SlidingSync extends TypedEventEmitter Date: Thu, 13 Oct 2022 14:09:33 +0100 Subject: [PATCH 48/48] Emit events when setting unread notifications (#2748) Co-authored-by: Travis Ralston --- spec/unit/notifications.spec.ts | 26 +++++++++-- spec/unit/room.spec.ts | 48 +++++++++++++++++-- src/models/room.ts | 83 +++++++++++++++++++++++++++++---- 3 files changed, 141 insertions(+), 16 deletions(-) diff --git a/spec/unit/notifications.spec.ts b/spec/unit/notifications.spec.ts index 89601327b..def7ef820 100644 --- a/spec/unit/notifications.spec.ts +++ b/spec/unit/notifications.spec.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Feature, ServerSupport } from "../../src/feature"; import { EventType, fixNotificationCountOnDecryption, @@ -23,6 +24,7 @@ import { NotificationCountType, RelationType, Room, + RoomEvent, } from "../../src/matrix"; import { IActionsObject } from "../../src/pushprocessor"; import { ReEmitter } from "../../src/ReEmitter"; @@ -56,8 +58,12 @@ describe("fixNotificationCountOnDecryption", () => { supportsExperimentalThreads: jest.fn().mockReturnValue(true), }); mockClient.reEmitter = mock(ReEmitter, 'ReEmitter'); + mockClient.canSupport = new Map(); + Object.keys(Feature).forEach(feature => { + mockClient.canSupport.set(feature as Feature, ServerSupport.Stable); + }); - room = new Room(ROOM_ID, mockClient, mockClient.getUserId()); + room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? ""); room.setUnreadNotificationCount(NotificationCountType.Total, 1); room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); @@ -93,12 +99,12 @@ describe("fixNotificationCountOnDecryption", () => { }); it("changes the room count to highlight on decryption", () => { - expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(1); + expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2); expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0); fixNotificationCountOnDecryption(mockClient, event); - expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(1); + expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2); expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1); }); @@ -111,4 +117,18 @@ describe("fixNotificationCountOnDecryption", () => { expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1); expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1); }); + + it("emits events", () => { + const cb = jest.fn(); + room.on(RoomEvent.UnreadNotifications, cb); + + room.setUnreadNotificationCount(NotificationCountType.Total, 1); + expect(cb).toHaveBeenLastCalledWith({ highlight: 0, total: 1 }); + + room.setUnreadNotificationCount(NotificationCountType.Highlight, 5); + expect(cb).toHaveBeenLastCalledWith({ highlight: 5, total: 1 }); + + room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 5); + expect(cb).toHaveBeenLastCalledWith({ highlight: 5 }, "$123"); + }); }); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index de9f0c5f9..902437e0b 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -2572,15 +2572,15 @@ describe("Room", function() { }); it("defaults to undefined", () => { - expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBeUndefined(); - expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined(); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(0); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0); }); it("lets you set values", () => { room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1); expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1); - expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined(); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0); room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 10); @@ -2592,10 +2592,48 @@ describe("Room", function() { room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666); room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123); + expect(room.getThreadsAggregateNotificationType()).toBe(NotificationCountType.Highlight); + room.resetThreadUnreadNotificationCount(); - expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBeUndefined(); - expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined(); + expect(room.getThreadsAggregateNotificationType()).toBe(null); + + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(0); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0); + }); + + it("sets the room threads notification type", () => { + room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666); + expect(room.getThreadsAggregateNotificationType()).toBe(NotificationCountType.Total); + room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123); + expect(room.getThreadsAggregateNotificationType()).toBe(NotificationCountType.Highlight); + room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 333); + expect(room.getThreadsAggregateNotificationType()).toBe(NotificationCountType.Highlight); + }); + }); + + describe("hasThreadUnreadNotification", () => { + it('has no notifications by default', () => { + expect(room.hasThreadUnreadNotification()).toBe(false); + }); + + it('main timeline notification does not affect this', () => { + room.setUnreadNotificationCount(NotificationCountType.Highlight, 1); + expect(room.hasThreadUnreadNotification()).toBe(false); + room.setUnreadNotificationCount(NotificationCountType.Total, 1); + expect(room.hasThreadUnreadNotification()).toBe(false); + + room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1); + expect(room.hasThreadUnreadNotification()).toBe(true); + }); + + it('lets you reset', () => { + room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 1); + expect(room.hasThreadUnreadNotification()).toBe(true); + + room.resetThreadUnreadNotificationCount(); + + expect(room.hasThreadUnreadNotification()).toBe(false); }); }); }); diff --git a/src/models/room.ts b/src/models/room.ts index aa1ffdd74..b1019dc4e 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -57,6 +57,7 @@ import { ReceiptContent, synthesizeReceipt, } from "./read-receipt"; +import { Feature, ServerSupport } from "../feature"; // 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 @@ -96,7 +97,7 @@ export interface IRecommendedVersion { // price to pay to keep matrix-js-sdk responsive. const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30; -type NotificationCount = Partial>; +export type NotificationCount = Partial>; export enum NotificationCountType { Highlight = "highlight", @@ -127,6 +128,7 @@ export enum RoomEvent { OldStateUpdated = "Room.OldStateUpdated", CurrentStateUpdated = "Room.CurrentStateUpdated", HistoryImportedWithinTimeline = "Room.historyImportedWithinTimeline", + UnreadNotifications = "Room.UnreadNotifications", } type EmittedEvents = RoomEvent @@ -164,6 +166,10 @@ export type RoomEventHandlerMap = { markerEvent: MatrixEvent, room: Room, ) => void; + [RoomEvent.UnreadNotifications]: ( + unreadNotifications: NotificationCount, + threadId?: string, + ) => void; [RoomEvent.TimelineRefresh]: (room: Room, eventTimelineSet: EventTimelineSet) => void; [ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void; } & ThreadHandlerMap @@ -186,7 +192,8 @@ export class Room extends ReadReceipt { public readonly reEmitter: TypedReEmitter; private txnToEvent: Record = {}; // Pending in-flight requests { string: MatrixEvent } private notificationCounts: NotificationCount = {}; - private threadNotifications: Map = new Map(); + private readonly threadNotifications = new Map(); + private roomThreadsNotificationType: NotificationCountType | null = null; private readonly timelineSets: EventTimelineSet[]; public readonly threadsTimelineSets: EventTimelineSet[] = []; // any filtered timeline sets we're maintaining for this room @@ -1182,38 +1189,97 @@ export class Room extends ReadReceipt { * @return {Number} The notification count, or undefined if there is no count * for this type. */ - public getUnreadNotificationCount(type = NotificationCountType.Total): number | undefined { - return this.notificationCounts[type]; + public getUnreadNotificationCount(type = NotificationCountType.Total): number { + let count = this.notificationCounts[type] ?? 0; + if (this.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) { + for (const threadNotification of this.threadNotifications.values()) { + count += threadNotification[type] ?? 0; + } + } + return count; } /** + * @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' * @returns The notification count, or undefined if there is no count * for this type. */ - public getThreadUnreadNotificationCount(threadId: string, type = NotificationCountType.Total): number | undefined { - return this.threadNotifications.get(threadId)?.[type]; + public getThreadUnreadNotificationCount(threadId: string, type = NotificationCountType.Total): number { + return this.threadNotifications.get(threadId)?.[type] ?? 0; } /** + * @experimental + * Checks if the current room has unread thread notifications + * @returns {boolean} + */ + public hasThreadUnreadNotification(): boolean { + for (const notification of this.threadNotifications.values()) { + if ((notification.highlight ?? 0) > 0 || (notification.total ?? 0) > 0) { + return true; + } + } + return false; + } + + /** + * @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' * @returns {void} */ public setThreadUnreadNotificationCount(threadId: string, type: NotificationCountType, count: number): void { - this.threadNotifications.set(threadId, { + const notification: NotificationCount = { highlight: this.threadNotifications.get(threadId)?.highlight, total: this.threadNotifications.get(threadId)?.total, ...{ [type]: count, }, - }); + }; + + this.threadNotifications.set(threadId, notification); + + // Remember what the global threads notification count type is + // for all the threads in the room + if (count > 0) { + switch (this.roomThreadsNotificationType) { + case NotificationCountType.Highlight: + break; + case NotificationCountType.Total: + if (type === NotificationCountType.Highlight) { + this.roomThreadsNotificationType = type; + } + break; + default: + this.roomThreadsNotificationType = type; + } + } + + this.emit( + RoomEvent.UnreadNotifications, + notification, + threadId, + ); } + /** + * @experimental + * @returns the notification count type for all the threads in the room + */ + public getThreadsAggregateNotificationType(): NotificationCountType | null { + return this.roomThreadsNotificationType; + } + + /** + * @experimental + * Resets the thread notifications for this room + */ public resetThreadUnreadNotificationCount(): void { + this.roomThreadsNotificationType = null; this.threadNotifications.clear(); } @@ -1224,6 +1290,7 @@ export class Room extends ReadReceipt { */ public setUnreadNotificationCount(type: NotificationCountType, count: number): void { this.notificationCounts[type] = count; + this.emit(RoomEvent.UnreadNotifications, this.notificationCounts); } public setSummary(summary: IRoomSummary): void {