diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..2c068fff3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @matrix-org/element-web diff --git a/.github/workflows/preview_changelog.yaml b/.github/workflows/preview_changelog.yaml new file mode 100644 index 000000000..d68d19361 --- /dev/null +++ b/.github/workflows/preview_changelog.yaml @@ -0,0 +1,12 @@ +name: Preview Changelog +on: + pull_request_target: + types: [ opened, edited, labeled ] +jobs: + changelog: + runs-on: ubuntu-latest + steps: + - name: Preview Changelog + uses: matrix-org/allchange@main + with: + ghToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index f352afcf2..a4f3eb273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +Changes in [12.2.0](https://github.com/vector-im/element-desktop/releases/tag/v12.2.0) (2021-07-02) +=================================================================================================== + +## ✨ Features + * Improve calculateRoomName performances by using Intl.Collator + [\#1801](https://github.com/matrix-org/matrix-js-sdk/pull/1801) + * Switch callEventHandler from listening on `event` to `Room.timeline` + [\#1789](https://github.com/matrix-org/matrix-js-sdk/pull/1789) + * Expose MatrixEvent's internal clearEvent as a function + [\#1784](https://github.com/matrix-org/matrix-js-sdk/pull/1784) + +## 🐛 Bug Fixes + * Clean up Event.clearEvent handling to fix a bug where malformed events with falsey content wouldn't be considered decrypted + [\#1807](https://github.com/matrix-org/matrix-js-sdk/pull/1807) + * Standardise spelling and casing of homeserver, identity server, and integration manager + [\#1782](https://github.com/matrix-org/matrix-js-sdk/pull/1782) + Changes in [12.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.1.0) (2021-07-19) ================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.1.0-rc.1...v12.1.0) diff --git a/package.json b/package.json index 854021a71..63c9e731e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "12.1.0", + "version": "12.2.0", "description": "Matrix Client-Server SDK for Javascript", "scripts": { "prepublishOnly": "yarn build", @@ -11,7 +11,7 @@ "build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", "build:types": "tsc --emitDeclarationOnly", "build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src", - "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", + "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", "build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js", "gendoc": "jsdoc -c jsdoc.json -P package.json", "lint": "yarn lint:types && yarn lint:js", @@ -81,14 +81,15 @@ "@types/request": "^2.48.5", "@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/parser": "^4.17.0", + "allchange": "github:matrix-org/allchange", "babel-jest": "^26.6.3", "babelify": "^10.0.0", - "better-docs": "^2.3.2", + "better-docs": "^2.4.0-beta.9", "browserify": "^17.0.0", "docdash": "^1.2.0", "eslint": "7.18.0", "eslint-config-google": "^0.14.0", - "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main", + "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945", "exorcist": "^1.0.1", "fake-indexeddb": "^3.1.2", "jest": "^26.6.3", diff --git a/release.sh b/release.sh index e2ef78263..af42fe886 100755 --- a/release.sh +++ b/release.sh @@ -102,11 +102,6 @@ yarn cache clean # Ensure all dependencies are updated yarn install --ignore-scripts --pure-lockfile -if [ -z "$skip_changelog" ]; then - # update_changelog doesn't have a --version flag - update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit) -fi - # Login and publish continues to use `npm`, as it seems to have more clearly # defined options and semantics than `yarn` for writing to the registry. if [ -z "$skip_npm" ]; then @@ -133,14 +128,6 @@ if [ $prerelease -eq 1 ]; then echo Making a PRE-RELEASE fi -if [ -z "$skip_changelog" ]; then - if ! command -v update_changelog >/dev/null 2>&1; then - echo "release.sh requires github-changelog-generator. Try:" >&2 - echo " pip install git+https://github.com/matrix-org/github-changelog-generator.git" >&2 - exit 1 - fi -fi - # we might already be on the release branch, in which case, yay # If we're on any branch starting with 'release', we don't create # a separate release branch (this allows us to use the same @@ -156,7 +143,7 @@ fi if [ -z "$skip_changelog" ]; then echo "Generating changelog" - update_changelog -f "$changelog_file" "$release" + yarn run allchange "$release" read -p "Edit $changelog_file manually, or press enter to continue " REPLY if [ -n "$(git ls-files --modified $changelog_file)" ]; then diff --git a/spec/integ/megolm-integ.spec.js b/spec/integ/megolm-integ.spec.js index e2bc34c25..f2150743e 100644 --- a/spec/integ/megolm-integ.spec.js +++ b/spec/integ/megolm-integ.spec.js @@ -1025,4 +1025,68 @@ describe("megolm", function() { }); }); }); + + it("Alice can decrypt a message with falsey content", function() { + return aliceTestClient.start().then(() => { + return createOlmSession(testOlmAccount, aliceTestClient); + }).then((p2pSession) => { + const groupSession = new Olm.OutboundGroupSession(); + groupSession.create(); + + // make the room_key event + const roomKeyEncrypted = encryptGroupSessionKey({ + senderKey: testSenderKey, + recipient: aliceTestClient, + p2pSession: p2pSession, + groupSession: groupSession, + room_id: ROOM_ID, + }); + + const plaintext = { + type: "m.room.message", + content: undefined, + room_id: ROOM_ID, + }; + + const messageEncrypted = { + event_id: 'test_megolm_event', + content: { + algorithm: "m.megolm.v1.aes-sha2", + ciphertext: groupSession.encrypt(JSON.stringify(plaintext)), + device_id: "testDevice", + sender_key: testSenderKey, + session_id: groupSession.session_id(), + }, + type: "m.room.encrypted", + }; + + // Alice gets both the events in a single sync + const syncResponse = { + next_batch: 1, + to_device: { + events: [roomKeyEncrypted], + }, + rooms: { + join: {}, + }, + }; + syncResponse.rooms.join[ROOM_ID] = { + timeline: { + events: [messageEncrypted], + }, + }; + + aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); + return aliceTestClient.flushSync(); + }).then(function() { + const room = aliceTestClient.client.getRoom(ROOM_ID); + const event = room.getLiveTimeline().getEvents()[0]; + expect(event.isEncrypted()).toBe(true); + return testUtils.awaitDecryption(event); + }).then((event) => { + expect(event.getRoomId()).toEqual(ROOM_ID); + expect(event.getContent()).toEqual({}); + expect(event.getClearContent()).toBeUndefined(); + }); + }); }); diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index 816b952b1..878f79fbd 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -274,7 +274,7 @@ describe("Crypto", function() { // alice encrypts each event, and then bob tries to decrypt // them without any keys, so that they'll be in pending await aliceClient.crypto.encryptEvent(event, aliceRoom); - event.clearEvent = {}; + event.clearEvent = undefined; event.senderCurve25519Key = null; event.claimedEd25519Key = null; try { diff --git a/spec/unit/models/MSC3089Branch.spec.ts b/spec/unit/models/MSC3089Branch.spec.ts index fc8b35815..72454cc54 100644 --- a/spec/unit/models/MSC3089Branch.spec.ts +++ b/spec/unit/models/MSC3089Branch.spec.ts @@ -16,7 +16,6 @@ limitations under the License. import { MatrixClient } from "../../../src"; import { Room } from "../../../src/models/room"; -import { MatrixEvent } from "../../../src/models/event"; import { UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event"; import { EventTimelineSet } from "../../../src/models/event-timeline-set"; import { EventTimeline } from "../../../src/models/event-timeline"; @@ -25,7 +24,7 @@ import { MSC3089Branch } from "../../../src/models/MSC3089Branch"; describe("MSC3089Branch", () => { let client: MatrixClient; // @ts-ignore - TS doesn't know that this is a type - let indexEvent: MatrixEvent; + let indexEvent: any; let branch: MSC3089Branch; const branchRoomId = "!room:example.org"; @@ -47,10 +46,10 @@ describe("MSC3089Branch", () => { } }, }; - indexEvent = { + indexEvent = ({ getRoomId: () => branchRoomId, getStateKey: () => fileEventId, - }; + }); branch = new MSC3089Branch(client, indexEvent); }); diff --git a/spec/unit/models/MSC3089TreeSpace.spec.ts b/spec/unit/models/MSC3089TreeSpace.spec.ts index 951ab4c0e..ae9652e7d 100644 --- a/spec/unit/models/MSC3089TreeSpace.spec.ts +++ b/spec/unit/models/MSC3089TreeSpace.spec.ts @@ -29,7 +29,7 @@ import { MatrixError } from "../../../src/http-api"; describe("MSC3089TreeSpace", () => { let client: MatrixClient; - let room: Room; + let room: any; let tree: MSC3089TreeSpace; const roomId = "!tree:localhost"; const targetUser = "@target:example.org"; @@ -170,7 +170,7 @@ describe("MSC3089TreeSpace", () => { expect(userIds).toMatchObject([target]); return Promise.resolve(); }); - client.invite = () => Promise.resolve(); // we're not testing this here - see other tests + client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests client.sendSharedHistoryKeys = sendKeysFn; // Mock the history check as best as possible @@ -198,7 +198,7 @@ describe("MSC3089TreeSpace", () => { expect(userIds).toMatchObject([target]); return Promise.resolve(); }); - client.invite = () => Promise.resolve(); // we're not testing this here - see other tests + client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests client.sendSharedHistoryKeys = sendKeysFn; const historyVis = "joined"; // NOTE: Changed. @@ -446,9 +446,9 @@ describe("MSC3089TreeSpace", () => { // Danger: these are partial implementations for testing purposes only // @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important - let childState: { [roomId: string]: MatrixEvent[] } = {}; + let childState: { [roomId: string]: any[] } = {}; // @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important - let parentState: MatrixEvent[] = []; + let parentState: any[] = []; let parentRoom: Room; let childTrees: MSC3089TreeSpace[]; let rooms: { [roomId: string]: Room }; diff --git a/spec/unit/relations.spec.ts b/spec/unit/relations.spec.ts index 45c20c00c..27370fba0 100644 --- a/spec/unit/relations.spec.ts +++ b/spec/unit/relations.spec.ts @@ -16,11 +16,13 @@ limitations under the License. import { EventTimelineSet } from "../../src/models/event-timeline-set"; import { MatrixEvent } from "../../src/models/event"; +import { Room } from "../../src/models/room"; import { Relations } from "../../src/models/relations"; describe("Relations", function() { it("should deduplicate annotations", function() { - const relations = new Relations("m.annotation", "m.reaction"); + const room = new Room("room123", null, null); + const relations = new Relations("m.annotation", "m.reaction", room); // Create an instance of an annotation const eventData = { @@ -95,10 +97,8 @@ describe("Relations", function() { }); // Stub the room - const room = { - getPendingEvent() { return null; }, - getUnfilteredTimelineSet() { return null; }, - }; + + const room = new Room("room123", null, null); // Add the target event first, then the relation event { diff --git a/spec/unit/utils.spec.ts b/spec/unit/utils.spec.ts index 9611511b4..12a83b235 100644 --- a/spec/unit/utils.spec.ts +++ b/spec/unit/utils.spec.ts @@ -493,4 +493,68 @@ describe("utils", function() { expect(deepSortedObjectEntries(input)).toMatchObject(output); }); }); + + describe("recursivelyAssign", () => { + it("doesn't override with null/undefined", () => { + const result = utils.recursivelyAssign( + { + string: "Hello world", + object: {}, + float: 0.1, + }, { + string: null, + object: undefined, + }, + true, + ); + + expect(result).toStrictEqual({ + string: "Hello world", + object: {}, + float: 0.1, + }); + }); + + it("assigns recursively", () => { + const result = utils.recursivelyAssign( + { + number: 42, + object: { + message: "Hello world", + day: "Monday", + langs: { + compiled: ["c++"], + }, + }, + thing: "string", + }, { + number: 2, + object: { + message: "How are you", + day: "Friday", + langs: { + compiled: ["c++", "c"], + }, + }, + thing: { + aSubThing: "something", + }, + }, + ); + + expect(result).toStrictEqual({ + number: 2, + object: { + message: "How are you", + day: "Friday", + langs: { + compiled: ["c++", "c"], + }, + }, + thing: { + aSubThing: "something", + }, + }); + }); + }); }); diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index 100f334ec..400c5ecb3 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -16,6 +16,7 @@ limitations under the License. import { TestClient } from '../../TestClient'; import { MatrixCall, CallErrorCode, CallEvent } from '../../../src/webrtc/call'; +import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes'; const DUMMY_SDP = ( "v=0\r\n" + @@ -298,4 +299,68 @@ describe('Call', function() { // Hangup to stop timers call.hangup(CallErrorCode.UserHangup, true); }); + + it("should map SDPStreamMetadata to feeds", async () => { + const callPromise = call.placeVoiceCall(); + await client.httpBackend.flush(); + await callPromise; + + call.getOpponentMember = () => { + return { userId: "@bob:bar.uk" }; + }; + + await call.onAnswerReceived({ + getContent: () => { + return { + version: 1, + call_id: call.callId, + party_id: 'party_id', + answer: { + sdp: DUMMY_SDP, + }, + [SDPStreamMetadataKey]: { + "stream_id": { + purpose: SDPStreamMetadataPurpose.Usermedia, + audio_muted: true, + video_muted: false, + }, + }, + }; + }, + }); + + call.pushRemoteFeed({ id: "stream_id", getAudioTracks: () => ["track1"], getVideoTracks: () => ["track1"] }); + const feed = call.getFeeds().find((feed) => feed.stream.id === "stream_id"); + expect(feed?.purpose).toBe(SDPStreamMetadataPurpose.Usermedia); + expect(feed?.isAudioMuted()).toBeTruthy(); + expect(feed?.isVideoMuted()).not.toBeTruthy(); + }); + + it("should fallback to replaceTrack() if the other side doesn't support SPDStreamMetadata", async () => { + const callPromise = call.placeVoiceCall(); + await client.httpBackend.flush(); + await callPromise; + + call.getOpponentMember = () => { + return { userId: "@bob:bar.uk" }; + }; + + await call.onAnswerReceived({ + getContent: () => { + return { + version: 1, + call_id: call.callId, + party_id: 'party_id', + answer: { + sdp: DUMMY_SDP, + }, + }; + }, + }); + + call.setScreensharingEnabledWithoutMetadataSupport = jest.fn(); + + call.setScreensharingEnabled(true); + expect(call.setScreensharingEnabledWithoutMetadataSupport).toHaveBeenCalled(); + }); }); diff --git a/src/@types/event.ts b/src/@types/event.ts index 42c7d0529..56ac83d8a 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -53,6 +53,8 @@ export enum EventType { CallReject = "m.call.reject", CallSelectAnswer = "m.call.select_answer", CallNegotiate = "m.call.negotiate", + CallSDPStreamMetadataChanged = "m.call.sdp_stream_metadata_changed", + CallSDPStreamMetadataChangedPrefix = "org.matrix.call.sdp_stream_metadata_changed", CallReplaces = "m.call.replaces", CallAssertedIdentity = "m.call.asserted_identity", CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity", diff --git a/src/@types/partials.ts b/src/@types/partials.ts index 19c11d153..f66dbe971 100644 --- a/src/@types/partials.ts +++ b/src/@types/partials.ts @@ -74,3 +74,11 @@ export enum HistoryVisibility { Shared = "shared", WorldReadable = "world_readable", } + +// XXX move to OlmDevice when converted +export interface InboundGroupSessionData { + room_id: string; // eslint-disable-line camelcase + session: string; + keysClaimed: Record; + forwardingCurve25519KeyChain: string[]; +} diff --git a/src/client.ts b/src/client.ts index 45d1de013..0733c5a88 100644 --- a/src/client.ts +++ b/src/client.ts @@ -89,9 +89,6 @@ import { IRecoveryKey, ISecretStorageKeyInfo, } from "./crypto/api"; -import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store"; -import { LocalStorageCryptoStore } from "./crypto/store/localStorage-crypto-store"; -import { IndexedDBCryptoStore } from "./crypto/store/indexeddb-crypto-store"; import { SyncState } from "./sync.api"; import { EventTimelineSet } from "./models/event-timeline-set"; import { VerificationRequest } from "./crypto/verification/request/VerificationRequest"; @@ -146,10 +143,10 @@ import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@ import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "./@types/spaces"; import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules"; import { IThreepid } from "./@types/threepids"; +import { CryptoStore } from "./crypto/store/base"; export type Store = IStore; export type SessionStore = WebStorageSessionStore; -export type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore; export type Callback = (err: Error | any | null, data?: any) => void; export type ResetTimelineCallback = (roomId: string) => boolean; @@ -5187,11 +5184,11 @@ export class MatrixClient extends EventEmitter { * The operation also updates MatrixClient.pushRules at the end. * @param {string} scope "global" or device-specific. * @param {string} roomId the id of the room. - * @param {string} mute the mute state. + * @param {boolean} mute the mute state. * @return {Promise} Resolves: result object * @return {module:http-api.MatrixError} Rejects: with an error response. */ - public setRoomMutePushRule(scope: string, roomId: string, mute: string): Promise | void { + public setRoomMutePushRule(scope: string, roomId: string, mute: boolean): Promise | void { let deferred; let hasDontNotifyRule; diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index aacb47148..76f651209 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -28,10 +28,11 @@ import { decryptAES, encryptAES } from './aes'; import { PkSigning } from "@matrix-org/olm"; import { DeviceInfo } from "./deviceinfo"; import { SecretStorage } from "./SecretStorage"; -import { CryptoStore, ICrossSigningKey, ISignedKey, MatrixClient } from "../client"; +import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client"; import { OlmDevice } from "./OlmDevice"; import { ICryptoCallbacks } from "../matrix"; import { ISignatures } from "../@types/signed"; +import { CryptoStore } from "./store/base"; const KEY_REQUEST_TIMEOUT_MS = 1000 * 60; @@ -47,6 +48,12 @@ export interface ICacheCallbacks { storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise; } +export interface ICrossSigningInfo { + keys: Record; + firstUse: boolean; + crossSigningVerifiedBefore: boolean; +} + export class CrossSigningInfo extends EventEmitter { public keys: Record = {}; public firstUse = true; @@ -75,7 +82,7 @@ export class CrossSigningInfo extends EventEmitter { super(); } - public static fromStorage(obj: object, userId: string): CrossSigningInfo { + public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo { const res = new CrossSigningInfo(userId); for (const prop in obj) { if (obj.hasOwnProperty(prop)) { @@ -85,7 +92,7 @@ export class CrossSigningInfo extends EventEmitter { return res; } - public toStorage(): object { + public toStorage(): ICrossSigningInfo { return { keys: this.keys, firstUse: this.firstUse, diff --git a/src/crypto/DeviceList.ts b/src/crypto/DeviceList.ts index 890f3bfa3..d27af3c54 100644 --- a/src/crypto/DeviceList.ts +++ b/src/crypto/DeviceList.ts @@ -24,12 +24,13 @@ import { EventEmitter } from 'events'; import { logger } from '../logger'; import { DeviceInfo, IDevice } from './deviceinfo'; -import { CrossSigningInfo } from './CrossSigning'; +import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning'; import * as olmlib from './olmlib'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { chunkPromises, defer, IDeferred, sleep } from '../utils'; -import { MatrixClient, CryptoStore } from "../client"; +import { MatrixClient } from "../client"; import { OlmDevice } from "./OlmDevice"; +import { CryptoStore } from "./store/base"; /* State transition diagram for DeviceList.deviceTrackingStatus * @@ -52,7 +53,7 @@ import { OlmDevice } from "./OlmDevice"; */ // constants for DeviceList.deviceTrackingStatus -enum TrackingStatus { +export enum TrackingStatus { NotTracked, PendingDownload, DownloadInProgress, @@ -65,32 +66,22 @@ export type DeviceInfoMap = Record>; * @alias module:crypto/DeviceList */ export class DeviceList extends EventEmitter { - // userId -> { - // deviceId -> { - // [device info] - // } - // } - private devices: Record> = {}; + private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {}; - // userId -> { - // [key info] - // } - public crossSigningInfo: Record = {}; + public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {}; // map of identity keys to the user who owns it private userByIdentityKey: Record = {}; // which users we are tracking device status for. - // userId -> TRACKING_STATUS_* - private deviceTrackingStatus: Record = {}; // loaded from storage in load() + private deviceTrackingStatus: { [userId: string]: TrackingStatus } = {}; // loaded from storage in load() // The 'next_batch' sync token at the point the data was written, // ie. a token representing the point immediately after the // moment represented by the snapshot in the db. private syncToken: string = null; - // userId -> promise - private keyDownloadsInProgressByUser: Record> = {}; + private keyDownloadsInProgressByUser: { [userId: string]: Promise } = {}; // Set whenever changes are made other than setting the sync token private dirty = false; @@ -375,7 +366,7 @@ export class DeviceList extends EventEmitter { return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId); } - public storeCrossSigningForUser(userId: string, info: CrossSigningInfo): void { + public storeCrossSigningForUser(userId: string, info: ICrossSigningInfo): void { this.crossSigningInfo[userId] = info; this.dirty = true; } @@ -603,7 +594,7 @@ export class DeviceList extends EventEmitter { } } - public setRawStoredCrossSigningForUser(userId: string, info: object): void { + public setRawStoredCrossSigningForUser(userId: string, info: ICrossSigningInfo): void { this.crossSigningInfo[userId] = info; } @@ -864,9 +855,7 @@ class DeviceListUpdateSerialiser { crossSigning.setKeys(crossSigningResponse); - this.deviceList.setRawStoredCrossSigningForUser( - userId, crossSigning.toStorage(), - ); + this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage()); // NB. Unlike most events in the js-sdk, this one is internal to the // js-sdk and is not re-emitted diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index b0410df72..4d8d2618a 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -949,7 +949,7 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) { * data stored in the session store about an inbound group session * * @typedef {Object} InboundGroupSessionData - * @property {string} room_Id + * @property {string} room_id * @property {string} session pickled Olm.InboundGroupSession * @property {Object} keysClaimed * @property {Array} forwardingCurve25519KeyChain Devices involved in forwarding diff --git a/src/crypto/OutgoingRoomKeyRequestManager.ts b/src/crypto/OutgoingRoomKeyRequestManager.ts index d01245722..e053d8072 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.ts +++ b/src/crypto/OutgoingRoomKeyRequestManager.ts @@ -15,9 +15,9 @@ limitations under the License. */ import { logger } from '../logger'; -import { CryptoStore, MatrixClient } from "../client"; +import { MatrixClient } from "../client"; import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index"; -import { OutgoingRoomKeyRequest } from './store/base'; +import { CryptoStore, OutgoingRoomKeyRequest } from './store/base'; import { EventType } from "../@types/event"; /** @@ -137,9 +137,7 @@ export class OutgoingRoomKeyRequestManager { recipients: IRoomKeyRequestRecipient[], resend = false, ): Promise { - const req = await this.cryptoStore.getOutgoingRoomKeyRequest( - requestBody, - ); + const req = await this.cryptoStore.getOutgoingRoomKeyRequest(requestBody); if (!req) { await this.cryptoStore.getOrAddOutgoingRoomKeyRequest({ requestBody: requestBody, @@ -237,10 +235,10 @@ export class OutgoingRoomKeyRequestManager { * @returns {Promise} resolves when the request has been updated in our * pending list. */ - public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { + public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { return this.cryptoStore.getOutgoingRoomKeyRequest( requestBody, - ).then((req) => { + ).then((req): unknown => { if (!req) { // no request was made for this key return; @@ -263,9 +261,7 @@ export class OutgoingRoomKeyRequestManager { 'deleting unnecessary room key request for ' + stringifyRequestBody(requestBody), ); - return this.cryptoStore.deleteOutgoingRoomKeyRequest( - req.requestId, RoomKeyRequestState.Unsent, - ); + return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent); case RoomKeyRequestState.Sent: { // send a cancellation. @@ -325,7 +321,7 @@ export class OutgoingRoomKeyRequestManager { * @return {Promise} resolves to a list of all the * {@link module:crypto/store/base~OutgoingRoomKeyRequest} */ - public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): OutgoingRoomKeyRequest[] { + public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): Promise { return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]); } @@ -415,7 +411,7 @@ export class OutgoingRoomKeyRequestManager { } // given a RoomKeyRequest, send it and update the request record - private sendOutgoingRoomKeyRequest(req: OutgoingRoomKeyRequest): Promise { + private sendOutgoingRoomKeyRequest(req: OutgoingRoomKeyRequest): Promise { logger.log( `Requesting keys for ${stringifyRequestBody(req.requestBody)}` + ` from ${stringifyRecipientList(req.recipients)}` + @@ -441,7 +437,7 @@ export class OutgoingRoomKeyRequestManager { // Given a RoomKeyRequest, cancel it and delete the request record unless // andResend is set, in which case transition to UNSENT. - private sendOutgoingRoomKeyRequestCancellation(req: OutgoingRoomKeyRequest, andResend = false): Promise { + private sendOutgoingRoomKeyRequestCancellation(req: OutgoingRoomKeyRequest, andResend = false): Promise { logger.log( `Sending cancellation for key request for ` + `${stringifyRequestBody(req.requestBody)} to ` + diff --git a/src/crypto/RoomList.ts b/src/crypto/RoomList.ts index b2835362f..24315d4ec 100644 --- a/src/crypto/RoomList.ts +++ b/src/crypto/RoomList.ts @@ -20,8 +20,8 @@ limitations under the License. * Manages the list of encrypted rooms */ +import { CryptoStore } from './store/base'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; -import { CryptoStore } from "../client"; /* eslint-disable camelcase */ export interface IRoomEncryption { diff --git a/src/crypto/aes.ts b/src/crypto/aes.ts index 736755b89..ba86ffe1b 100644 --- a/src/crypto/aes.ts +++ b/src/crypto/aes.ts @@ -50,12 +50,12 @@ async function encryptNode(data: string, key: Uint8Array, name: string, ivStr?: iv = decodeBase64(ivStr); } else { iv = crypto.randomBytes(16); - } - // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary - // (which would mean we wouldn't be able to decrypt on Android). The loss - // of a single bit of iv is a price we have to pay. - iv[8] &= 0x7f; + // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of iv is a price we have to pay. + iv[8] &= 0x7f; + } const [aesKey, hmacKey] = deriveKeysNode(key, name); @@ -137,12 +137,12 @@ async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr } else { iv = new Uint8Array(16); window.crypto.getRandomValues(iv); - } - // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary - // (which would mean we wouldn't be able to decrypt on Android). The loss - // of a single bit of iv is a price we have to pay. - iv[8] &= 0x7f; + // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of iv is a price we have to pay. + iv[8] &= 0x7f; + } const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); const encodedData = new TextEncoder().encode(data); diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index 2a1a2d8db..381f85188 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -61,7 +61,7 @@ interface IBlockedMap { }; } -interface IOlmDevice { +export interface IOlmDevice { userId: string; deviceInfo: T; } diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 761333f65..ea627dddf 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -59,12 +59,13 @@ import { IStore } from "../store"; import { Room } from "../models/room"; import { RoomMember } from "../models/room-member"; import { MatrixEvent } from "../models/event"; -import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client"; +import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey } from "../client"; import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base"; import type { IRoomEncryption, RoomList } from "./RoomList"; import { IRecoveryKey, IEncryptedEventInfo } from "./api"; import { IKeyBackupInfo } from "./keybackup"; import { ISyncStateData } from "../sync"; +import { CryptoStore } from "./store/base"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -1410,9 +1411,7 @@ export class Crypto extends EventEmitter { crossSigning.updateCrossSigningVerifiedBefore( this.checkUserTrust(userId).isCrossSigningVerified(), ); - this.deviceList.setRawStoredCrossSigningForUser( - userId, crossSigning.toStorage(), - ); + this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage()); } this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); diff --git a/src/crypto/store/base.ts b/src/crypto/store/base.ts index d76fb9ead..7ace6f2da 100644 --- a/src/crypto/store/base.ts +++ b/src/crypto/store/base.ts @@ -1,5 +1,33 @@ +/* +Copyright 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. +*/ + +import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index"; +import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager"; +import { ICrossSigningKey } from "../../client"; +import { IOlmDevice } from "../algorithms/megolm"; +import { TrackingStatus } from "../DeviceList"; +import { IRoomEncryption } from "../RoomList"; +import { IDevice } from "../deviceinfo"; +import { ICrossSigningInfo } from "../CrossSigning"; +import { PrefixedLogger } from "../../logger"; +import { InboundGroupSessionData } from "../../@types/partials"; +import { IEncryptedPayload } from "../aes"; + /** - * Internal module. Defintions for storage for the crypto module + * Internal module. Definitions for storage for the crypto module * * @module */ @@ -9,9 +37,141 @@ * * @interface CryptoStore */ +export interface CryptoStore { + startup(): Promise; + deleteAllData(): Promise; + getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise; + getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise; + getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise; + getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise; + getOutgoingRoomKeyRequestsByTarget( + userId: string, + deviceId: string, + wantedStates: number[], + ): Promise; + updateOutgoingRoomKeyRequest( + requestId: string, + expectedState: number, + updates: Partial, + ): Promise; + deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise; -import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index"; -import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager"; + // Olm Account + getAccount(txn: unknown, func: (accountPickle: string) => void); + storeAccount(txn: unknown, accountPickle: string): void; + getCrossSigningKeys(txn: unknown, func: (keys: Record) => void): void; + getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void; + storeCrossSigningKeys(txn: unknown, keys: Record): void; + storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void; + + // Olm Sessions + countEndToEndSessions(txn: unknown, func: (count: number) => void): void; + getEndToEndSession( + deviceKey: string, + sessionId: string, + txn: unknown, + func: (session: ISessionInfo) => void, + ): void; + getEndToEndSessions( + deviceKey: string, + txn: unknown, + func: (sessions: { [sessionId: string]: ISessionInfo }) => void, + ): void; + getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void; + storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void; + storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise; + getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise; + filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise; + + // Inbound Group Sessions + getEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + txn: unknown, + func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void, + ): void; + getAllEndToEndInboundGroupSessions( + txn: unknown, + func: (session: ISession | null) => void, + ): void; + addEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + sessionData: InboundGroupSessionData, + txn: unknown, + ): void; + storeEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + sessionData: InboundGroupSessionData, + txn: unknown, + ): void; + storeEndToEndInboundGroupSessionWithheld( + senderCurve25519Key: string, + sessionId: string, + sessionData: IWithheld, + txn: unknown, + ): void; + + // Device Data + getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void; + storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void; + storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void; + getEndToEndRooms(txn: unknown, func: (rooms: Record) => void): void; + getSessionsNeedingBackup(limit: number): Promise; + countSessionsNeedingBackup(txn?: unknown): Promise; + unmarkSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise; + markSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise; + addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string, txn?: unknown): void; + getSharedHistoryInboundGroupSessions( + roomId: string, + txn?: IDBTransaction, + ): Promise<[senderKey: string, sessionId: string][]>; + + // Session key backups + doTxn(mode: Mode, stores: Iterable, func: (txn: unknown) => T, log?: PrefixedLogger): Promise; +} + +export type Mode = "readonly" | "readwrite"; + +export interface ISession { + senderKey: string; + sessionId: string; + sessionData?: InboundGroupSessionData; +} + +export interface ISessionInfo { + deviceKey?: string; + sessionId?: string; + session?: string; + lastReceivedMessageTs?: number; +} + +export interface IDeviceData { + devices: { + [ userId: string ]: { + [ deviceId: string ]: IDevice; + }; + }; + trackingStatus: { + [ userId: string ]: TrackingStatus; + }; + crossSigningInfo?: Record; + syncToken?: string; +} + +export interface IProblem { + type: string; + fixed: boolean; + time: number; +} + +export interface IWithheld { + // eslint-disable-next-line camelcase + room_id: string; + code: string; + reason: string; +} /** * Represents an outgoing room key request diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.ts similarity index 73% rename from src/crypto/store/indexeddb-crypto-store-backend.js rename to src/crypto/store/indexeddb-crypto-store-backend.ts index 9a54a3537..20553f1e9 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.ts @@ -1,7 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2017 - 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. @@ -16,8 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { logger } from '../../logger'; +import { logger, PrefixedLogger } from '../../logger'; import * as utils from "../../utils"; +import { + CryptoStore, + IDeviceData, + IProblem, + ISession, + ISessionInfo, + IWithheld, + Mode, + OutgoingRoomKeyRequest, +} from "./base"; +import { IRoomKeyRequestBody } from "../index"; +import { ICrossSigningKey } from "../../client"; +import { IOlmDevice } from "../algorithms/megolm"; +import { IRoomEncryption } from "../RoomList"; +import { InboundGroupSessionData } from "../../@types/partials"; +import { IEncryptedPayload } from "../aes"; export const VERSION = 10; const PROFILE_TRANSACTIONS = false; @@ -29,23 +43,31 @@ const PROFILE_TRANSACTIONS = false; * * @implements {module:crypto/store/base~CryptoStore} */ -export class Backend { +export class Backend implements CryptoStore { + private nextTxnId = 0; + /** * @param {IDBDatabase} db */ - constructor(db) { - this._db = db; - this._nextTxnId = 0; - + constructor(private db: IDBDatabase) { // make sure we close the db on `onversionchange` - otherwise // attempts to delete the database will block (and subsequent // attempts to re-create it will also block). - db.onversionchange = (ev) => { - logger.log(`versionchange for indexeddb ${this._dbName}: closing`); + db.onversionchange = () => { + logger.log(`versionchange for indexeddb ${this.db.name}: closing`); db.close(); }; } + public async startup(): Promise { + // No work to do, as the startup is done by the caller (e.g IndexedDBCryptoStore) + // by passing us a ready IDBDatabase instance + return this; + } + public async deleteAllData(): Promise { + throw Error("This is not implemented, call IDBFactory::deleteDatabase(dbName) instead."); + } + /** * Look for an existing outgoing room key request, and if none is found, * add a new one @@ -56,11 +78,11 @@ export class Backend { * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ - getOrAddOutgoingRoomKeyRequest(request) { + public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise { const requestBody = request.requestBody; return new Promise((resolve, reject) => { - const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); + const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite"); txn.onerror = reject; // first see if we already have an entry for this request. @@ -99,9 +121,9 @@ export class Backend { * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if * not found */ - getOutgoingRoomKeyRequest(requestBody) { + public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { return new Promise((resolve, reject) => { - const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly"); txn.onerror = reject; this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => { @@ -122,7 +144,12 @@ export class Backend { * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if * not found. */ - _getOutgoingRoomKeyRequest(txn, requestBody, callback) { + // eslint-disable-next-line @typescript-eslint/naming-convention + private _getOutgoingRoomKeyRequest( + txn: IDBTransaction, + requestBody: IRoomKeyRequestBody, + callback: (req: OutgoingRoomKeyRequest | null) => void, + ): void { const store = txn.objectStore("outgoingRoomKeyRequests"); const idx = store.index("session"); @@ -131,8 +158,8 @@ export class Backend { requestBody.session_id, ]); - cursorReq.onsuccess = (ev) => { - const cursor = ev.target.result; + cursorReq.onsuccess = () => { + const cursor = cursorReq.result; if (!cursor) { // no match found callback(null); @@ -162,7 +189,7 @@ export class Backend { * there are no pending requests in those states. If there are multiple * requests in those states, an arbitrary one is chosen. */ - getOutgoingRoomKeyRequestByState(wantedStates) { + public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise { if (wantedStates.length === 0) { return Promise.resolve(null); } @@ -195,7 +222,7 @@ export class Backend { cursorReq.onsuccess = onsuccess; } - const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly"); const store = txn.objectStore("outgoingRoomKeyRequests"); const wantedState = wantedStates[stateIndex]; @@ -210,19 +237,23 @@ export class Backend { * @param {Number} wantedState * @return {Promise>} All elements in a given state */ - getAllOutgoingRoomKeyRequestsByState(wantedState) { + public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise { return new Promise((resolve, reject) => { - const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly"); const store = txn.objectStore("outgoingRoomKeyRequests"); const index = store.index("state"); const request = index.getAll(wantedState); - request.onsuccess = (ev) => resolve(ev.target.result); - request.onerror = (ev) => reject(ev.target.error); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); }); } - getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { + public getOutgoingRoomKeyRequestsByTarget( + userId: string, + deviceId: string, + wantedStates: number[], + ): Promise { let stateIndex = 0; const results = []; @@ -248,7 +279,7 @@ export class Backend { } } - const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly"); const store = txn.objectStore("outgoingRoomKeyRequests"); const wantedState = wantedStates[stateIndex]; @@ -270,7 +301,11 @@ export class Backend { * {@link module:crypto/store/base~OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ - updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { + public updateOutgoingRoomKeyRequest( + requestId: string, + expectedState: number, + updates: Partial, + ): Promise { let result = null; function onsuccess(ev) { @@ -291,9 +326,8 @@ export class Backend { result = data; } - const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); - const cursorReq = txn.objectStore("outgoingRoomKeyRequests") - .openCursor(requestId); + const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite"); + const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId); cursorReq.onsuccess = onsuccess; return promiseifyTxn(txn).then(() => result); } @@ -307,12 +341,14 @@ export class Backend { * * @returns {Promise} resolves once the operation is completed */ - deleteOutgoingRoomKeyRequest(requestId, expectedState) { - const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); - const cursorReq = txn.objectStore("outgoingRoomKeyRequests") - .openCursor(requestId); - cursorReq.onsuccess = (ev) => { - const cursor = ev.target.result; + public deleteOutgoingRoomKeyRequest( + requestId: string, + expectedState: number, + ): Promise { + const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite"); + const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId); + cursorReq.onsuccess = () => { + const cursor = cursorReq.result; if (!cursor) { return; } @@ -326,12 +362,12 @@ export class Backend { } cursor.delete(); }; - return promiseifyTxn(txn); + return promiseifyTxn(txn); } // Olm Account - getAccount(txn, func) { + public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void): void { const objectStore = txn.objectStore("account"); const getReq = objectStore.get("-"); getReq.onsuccess = function() { @@ -343,12 +379,12 @@ export class Backend { }; } - storeAccount(txn, newData) { + public storeAccount(txn: IDBTransaction, accountPickle: string): void { const objectStore = txn.objectStore("account"); - objectStore.put(newData, "-"); + objectStore.put(accountPickle, "-"); } - getCrossSigningKeys(txn, func) { + public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record) => void): void { const objectStore = txn.objectStore("account"); const getReq = objectStore.get("crossSigningKeys"); getReq.onsuccess = function() { @@ -360,7 +396,11 @@ export class Backend { }; } - getSecretStorePrivateKey(txn, func, type) { + public getSecretStorePrivateKey( + txn: IDBTransaction, + func: (key: IEncryptedPayload | null) => void, + type: string, + ): void { const objectStore = txn.objectStore("account"); const getReq = objectStore.get(`ssss_cache:${type}`); getReq.onsuccess = function() { @@ -372,19 +412,19 @@ export class Backend { }; } - storeCrossSigningKeys(txn, keys) { + public storeCrossSigningKeys(txn: IDBTransaction, keys: Record): void { const objectStore = txn.objectStore("account"); objectStore.put(keys, "crossSigningKeys"); } - storeSecretStorePrivateKey(txn, type, key) { + public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void { const objectStore = txn.objectStore("account"); objectStore.put(key, `ssss_cache:${type}`); } // Olm Sessions - countEndToEndSessions(txn, func) { + public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void { const objectStore = txn.objectStore("sessions"); const countReq = objectStore.count(); countReq.onsuccess = function() { @@ -396,7 +436,11 @@ export class Backend { }; } - getEndToEndSessions(deviceKey, txn, func) { + public getEndToEndSessions( + deviceKey: string, + txn: IDBTransaction, + func: (sessions: { [sessionId: string]: ISessionInfo }) => void, + ): void { const objectStore = txn.objectStore("sessions"); const idx = objectStore.index("deviceKey"); const getReq = idx.openCursor(deviceKey); @@ -419,7 +463,12 @@ export class Backend { }; } - getEndToEndSession(deviceKey, sessionId, txn, func) { + public getEndToEndSession( + deviceKey: string, + sessionId: string, + txn: IDBTransaction, + func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void, + ): void { const objectStore = txn.objectStore("sessions"); const getReq = objectStore.get([deviceKey, sessionId]); getReq.onsuccess = function() { @@ -438,7 +487,7 @@ export class Backend { }; } - getAllEndToEndSessions(txn, func) { + public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void { const objectStore = txn.objectStore("sessions"); const getReq = objectStore.openCursor(); getReq.onsuccess = function() { @@ -456,7 +505,12 @@ export class Backend { }; } - storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { + public storeEndToEndSession( + deviceKey: string, + sessionId: string, + sessionInfo: ISessionInfo, + txn: IDBTransaction, + ): void { const objectStore = txn.objectStore("sessions"); objectStore.put({ deviceKey, @@ -466,8 +520,8 @@ export class Backend { }); } - async storeEndToEndSessionProblem(deviceKey, type, fixed) { - const txn = this._db.transaction("session_problems", "readwrite"); + public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise { + const txn = this.db.transaction("session_problems", "readwrite"); const objectStore = txn.objectStore("session_problems"); objectStore.put({ deviceKey, @@ -478,13 +532,13 @@ export class Backend { return promiseifyTxn(txn); } - async getEndToEndSessionProblem(deviceKey, timestamp) { + public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise { let result; - const txn = this._db.transaction("session_problems", "readwrite"); + const txn = this.db.transaction("session_problems", "readwrite"); const objectStore = txn.objectStore("session_problems"); const index = objectStore.index("deviceKey"); const req = index.getAll(deviceKey); - req.onsuccess = (event) => { + req.onsuccess = () => { const problems = req.result; if (!problems.length) { result = null; @@ -511,14 +565,14 @@ export class Backend { } // FIXME: we should probably prune this when devices get deleted - async filterOutNotifiedErrorDevices(devices) { - const txn = this._db.transaction("notified_error_devices", "readwrite"); + public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise { + const txn = this.db.transaction("notified_error_devices", "readwrite"); const objectStore = txn.objectStore("notified_error_devices"); - const ret = []; + const ret: IOlmDevice[] = []; await Promise.all(devices.map((device) => { - return new Promise((resolve) => { + return new Promise((resolve) => { const { userId, deviceInfo } = device; const getReq = objectStore.get([userId, deviceInfo.deviceId]); getReq.onsuccess = function() { @@ -536,9 +590,14 @@ export class Backend { // Inbound group sessions - getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { - let session = false; - let withheld = false; + public getEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + txn: IDBTransaction, + func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void, + ): void { + let session: InboundGroupSessionData | boolean = false; + let withheld: IWithheld | boolean = false; const objectStore = txn.objectStore("inbound_group_sessions"); const getReq = objectStore.get([senderCurve25519Key, sessionId]); getReq.onsuccess = function() { @@ -549,7 +608,7 @@ export class Backend { session = null; } if (withheld !== false) { - func(session, withheld); + func(session as InboundGroupSessionData, withheld as IWithheld); } } catch (e) { abortWithException(txn, e); @@ -566,7 +625,7 @@ export class Backend { withheld = null; } if (session !== false) { - func(session, withheld); + func(session as InboundGroupSessionData, withheld as IWithheld); } } catch (e) { abortWithException(txn, e); @@ -574,7 +633,7 @@ export class Backend { }; } - getAllEndToEndInboundGroupSessions(txn, func) { + public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void { const objectStore = txn.objectStore("inbound_group_sessions"); const getReq = objectStore.openCursor(); getReq.onsuccess = function() { @@ -600,7 +659,12 @@ export class Backend { }; } - addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + public addEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + sessionData: InboundGroupSessionData, + txn: IDBTransaction, + ): void { const objectStore = txn.objectStore("inbound_group_sessions"); const addReq = objectStore.add({ senderCurve25519Key, sessionId, session: sessionData, @@ -623,23 +687,31 @@ export class Backend { }; } - storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + public storeEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + sessionData: InboundGroupSessionData, + txn: IDBTransaction, + ): void { const objectStore = txn.objectStore("inbound_group_sessions"); objectStore.put({ senderCurve25519Key, sessionId, session: sessionData, }); } - storeEndToEndInboundGroupSessionWithheld( - senderCurve25519Key, sessionId, sessionData, txn, - ) { + public storeEndToEndInboundGroupSessionWithheld( + senderCurve25519Key: string, + sessionId: string, + sessionData: IWithheld, + txn: IDBTransaction, + ): void { const objectStore = txn.objectStore("inbound_group_sessions_withheld"); objectStore.put({ senderCurve25519Key, sessionId, session: sessionData, }); } - getEndToEndDeviceData(txn, func) { + public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void { const objectStore = txn.objectStore("device_data"); const getReq = objectStore.get("-"); getReq.onsuccess = function() { @@ -651,24 +723,24 @@ export class Backend { }; } - storeEndToEndDeviceData(deviceData, txn) { + public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void { const objectStore = txn.objectStore("device_data"); objectStore.put(deviceData, "-"); } - storeEndToEndRoom(roomId, roomInfo, txn) { + public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void { const objectStore = txn.objectStore("rooms"); objectStore.put(roomInfo, roomId); } - getEndToEndRooms(txn, func) { + public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record) => void): void { const rooms = {}; const objectStore = txn.objectStore("rooms"); const getReq = objectStore.openCursor(); getReq.onsuccess = function() { const cursor = getReq.result; if (cursor) { - rooms[cursor.key] = cursor.value; + rooms[cursor.key as string] = cursor.value; cursor.continue(); } else { try { @@ -682,11 +754,11 @@ export class Backend { // session backups - getSessionsNeedingBackup(limit) { + public getSessionsNeedingBackup(limit: number): Promise { return new Promise((resolve, reject) => { const sessions = []; - const txn = this._db.transaction( + const txn = this.db.transaction( ["sessions_needing_backup", "inbound_group_sessions"], "readonly", ); @@ -716,9 +788,9 @@ export class Backend { }); } - countSessionsNeedingBackup(txn) { + public countSessionsNeedingBackup(txn?: IDBTransaction): Promise { if (!txn) { - txn = this._db.transaction("sessions_needing_backup", "readonly"); + txn = this.db.transaction("sessions_needing_backup", "readonly"); } const objectStore = txn.objectStore("sessions_needing_backup"); return new Promise((resolve, reject) => { @@ -728,12 +800,12 @@ export class Backend { }); } - unmarkSessionsNeedingBackup(sessions, txn) { + public async unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise { if (!txn) { - txn = this._db.transaction("sessions_needing_backup", "readwrite"); + txn = this.db.transaction("sessions_needing_backup", "readwrite"); } const objectStore = txn.objectStore("sessions_needing_backup"); - return Promise.all(sessions.map((session) => { + await Promise.all(sessions.map((session) => { return new Promise((resolve, reject) => { const req = objectStore.delete([session.senderKey, session.sessionId]); req.onsuccess = resolve; @@ -742,12 +814,12 @@ export class Backend { })); } - markSessionsNeedingBackup(sessions, txn) { + public async markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise { if (!txn) { - txn = this._db.transaction("sessions_needing_backup", "readwrite"); + txn = this.db.transaction("sessions_needing_backup", "readwrite"); } const objectStore = txn.objectStore("sessions_needing_backup"); - return Promise.all(sessions.map((session) => { + await Promise.all(sessions.map((session) => { return new Promise((resolve, reject) => { const req = objectStore.put({ senderCurve25519Key: session.senderKey, @@ -759,9 +831,14 @@ export class Backend { })); } - addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) { + public addSharedHistoryInboundGroupSession( + roomId: string, + senderKey: string, + sessionId: string, + txn?: IDBTransaction, + ): void { if (!txn) { - txn = this._db.transaction( + txn = this.db.transaction( "shared_history_inbound_group_sessions", "readwrite", ); } @@ -774,9 +851,12 @@ export class Backend { }; } - getSharedHistoryInboundGroupSessions(roomId, txn) { + public getSharedHistoryInboundGroupSessions( + roomId: string, + txn?: IDBTransaction, + ): Promise<[senderKey: string, sessionId: string][]> { if (!txn) { - txn = this._db.transaction( + txn = this.db.transaction( "shared_history_inbound_group_sessions", "readonly", ); } @@ -791,16 +871,21 @@ export class Backend { }); } - doTxn(mode, stores, func, log = logger) { + public doTxn( + mode: Mode, + stores: Iterable, + func: (txn: IDBTransaction) => T, + log: PrefixedLogger = logger, + ): Promise { let startTime; let description; if (PROFILE_TRANSACTIONS) { - const txnId = this._nextTxnId++; + const txnId = this.nextTxnId++; startTime = Date.now(); description = `${mode} crypto store transaction ${txnId} in ${stores}`; log.debug(`Starting ${description}`); } - const txn = this._db.transaction(stores, mode); + const txn = this.db.transaction(stores, mode); const promise = promiseifyTxn(txn); const result = func(txn); if (PROFILE_TRANSACTIONS) { @@ -818,7 +903,7 @@ export class Backend { } } -export function upgradeDatabase(db, oldVersion) { +export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void { logger.log( `Upgrading IndexedDBCryptoStore from version ${oldVersion}` + ` to ${VERSION}`, @@ -874,7 +959,7 @@ export function upgradeDatabase(db, oldVersion) { // Expand as needed. } -function createDatabase(db) { +function createDatabase(db: IDBDatabase): void { const outgoingRoomKeyRequestsStore = db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" }); @@ -887,15 +972,19 @@ function createDatabase(db) { outgoingRoomKeyRequestsStore.createIndex("state", "state"); } +interface IWrappedIDBTransaction extends IDBTransaction { + _mx_abortexception: Error; // eslint-disable-line camelcase +} + /* * Aborts a transaction with a given exception * The transaction promise will be rejected with this exception. */ -function abortWithException(txn, e) { +function abortWithException(txn: IDBTransaction, e: Error) { // We cheekily stick our exception onto the transaction object here // We could alternatively make the thing we pass back to the app // an object containing the transaction and exception. - txn._mx_abortexception = e; + (txn as IWrappedIDBTransaction)._mx_abortexception = e; try { txn.abort(); } catch (e) { @@ -904,28 +993,28 @@ function abortWithException(txn, e) { } } -function promiseifyTxn(txn) { +function promiseifyTxn(txn: IDBTransaction): Promise { return new Promise((resolve, reject) => { txn.oncomplete = () => { - if (txn._mx_abortexception !== undefined) { - reject(txn._mx_abortexception); + if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) { + reject((txn as IWrappedIDBTransaction)._mx_abortexception); } - resolve(); + resolve(null); }; txn.onerror = (event) => { - if (txn._mx_abortexception !== undefined) { - reject(txn._mx_abortexception); + if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) { + reject((txn as IWrappedIDBTransaction)._mx_abortexception); } else { logger.log("Error performing indexeddb txn", event); - reject(event.target.error); + reject(txn.error); } }; txn.onabort = (event) => { - if (txn._mx_abortexception !== undefined) { - reject(txn._mx_abortexception); + if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) { + reject((txn as IWrappedIDBTransaction)._mx_abortexception); } else { logger.log("Error performing indexeddb txn", event); - reject(event.target.error); + reject(txn.error); } }; }); diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.ts similarity index 62% rename from src/crypto/store/indexeddb-crypto-store.js rename to src/crypto/store/indexeddb-crypto-store.ts index 02a99e4e2..268d62128 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.ts @@ -1,7 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2017 - 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. @@ -16,12 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { logger } from '../../logger'; +import { logger, PrefixedLogger } from '../../logger'; import { LocalStorageCryptoStore } from './localStorage-crypto-store'; import { MemoryCryptoStore } from './memory-crypto-store'; import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend'; import { InvalidCryptoStoreError } from '../../errors'; import * as IndexedDBHelpers from "../../indexeddb-helpers"; +import { + CryptoStore, + IDeviceData, + IProblem, + ISession, + ISessionInfo, + IWithheld, + Mode, + OutgoingRoomKeyRequest, +} from "./base"; +import { IRoomKeyRequestBody } from "../index"; +import { ICrossSigningKey } from "../../client"; +import { IOlmDevice } from "../algorithms/megolm"; +import { IRoomEncryption } from "../RoomList"; +import { InboundGroupSessionData } from "../../@types/partials"; +import { IEncryptedPayload } from "../aes"; /** * Internal module. indexeddb storage for e2e. @@ -35,23 +49,30 @@ import * as IndexedDBHelpers from "../../indexeddb-helpers"; * * @implements {module:crypto/store/base~CryptoStore} */ -export class IndexedDBCryptoStore { +export class IndexedDBCryptoStore implements CryptoStore { + public static STORE_ACCOUNT = 'account'; + public static STORE_SESSIONS = 'sessions'; + public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; + public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld'; + public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions'; + public static STORE_DEVICE_DATA = 'device_data'; + public static STORE_ROOMS = 'rooms'; + public static STORE_BACKUP = 'sessions_needing_backup'; + + public static exists(indexedDB: IDBFactory, dbName: string): Promise { + return IndexedDBHelpers.exists(indexedDB, dbName); + } + + private backendPromise: Promise = null; + private backend: CryptoStore = null; + /** * Create a new IndexedDBCryptoStore * * @param {IDBFactory} indexedDB global indexedDB instance * @param {string} dbName name of db to connect to */ - constructor(indexedDB, dbName) { - this._indexedDB = indexedDB; - this._dbName = dbName; - this._backendPromise = null; - this._backend = null; - } - - static exists(indexedDB, dbName) { - return IndexedDBHelpers.exists(indexedDB, dbName); - } + constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {} /** * Ensure the database exists and is up-to-date, or fall back to @@ -62,25 +83,23 @@ export class IndexedDBCryptoStore { * @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend, * or a MemoryCryptoStore */ - startup() { - if (this._backendPromise) { - return this._backendPromise; + public startup(): Promise { + if (this.backendPromise) { + return this.backendPromise; } - this._backendPromise = new Promise((resolve, reject) => { - if (!this._indexedDB) { + this.backendPromise = new Promise((resolve, reject) => { + if (!this.indexedDB) { reject(new Error('no indexeddb support available')); return; } - logger.log(`connecting to indexeddb ${this._dbName}`); + logger.log(`connecting to indexeddb ${this.dbName}`); - const req = this._indexedDB.open( - this._dbName, IndexedDBCryptoStoreBackend.VERSION, - ); + const req = this.indexedDB.open(this.dbName, IndexedDBCryptoStoreBackend.VERSION); req.onupgradeneeded = (ev) => { - const db = ev.target.result; + const db = req.result; const oldVersion = ev.oldVersion; IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion); }; @@ -93,13 +112,13 @@ export class IndexedDBCryptoStore { req.onerror = (ev) => { logger.log("Error connecting to indexeddb", ev); - reject(ev.target.error); + reject(req.error); }; - req.onsuccess = (r) => { - const db = r.target.result; + req.onsuccess = () => { + const db = req.result; - logger.log(`connected to indexeddb ${this._dbName}`); + logger.log(`connected to indexeddb ${this.dbName}`); resolve(new IndexedDBCryptoStoreBackend.Backend(db)); }; }).then((backend) => { @@ -114,9 +133,7 @@ export class IndexedDBCryptoStore { ], (txn) => { backend.getEndToEndInboundGroupSession('', '', txn, () => {}); - }).then(() => { - return backend; - }, + }).then(() => backend, ); }).catch((e) => { if (e.name === 'VersionError') { @@ -126,7 +143,7 @@ export class IndexedDBCryptoStore { throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW); } logger.warn( - `unable to connect to indexeddb ${this._dbName}` + + `unable to connect to indexeddb ${this.dbName}` + `: falling back to localStorage store: ${e}`, ); @@ -139,10 +156,11 @@ export class IndexedDBCryptoStore { return new MemoryCryptoStore(); } }).then(backend => { - this._backend = backend; + this.backend = backend; + return backend as CryptoStore; }); - return this._backendPromise; + return this.backendPromise; } /** @@ -150,15 +168,15 @@ export class IndexedDBCryptoStore { * * @returns {Promise} resolves when the store has been cleared. */ - deleteAllData() { - return new Promise((resolve, reject) => { - if (!this._indexedDB) { + public deleteAllData(): Promise { + return new Promise((resolve, reject) => { + if (!this.indexedDB) { reject(new Error('no indexeddb support available')); return; } - logger.log(`Removing indexeddb instance: ${this._dbName}`); - const req = this._indexedDB.deleteDatabase(this._dbName); + logger.log(`Removing indexeddb instance: ${this.dbName}`); + const req = this.indexedDB.deleteDatabase(this.dbName); req.onblocked = () => { logger.log( @@ -168,11 +186,11 @@ export class IndexedDBCryptoStore { req.onerror = (ev) => { logger.log("Error deleting data from indexeddb", ev); - reject(ev.target.error); + reject(req.error); }; req.onsuccess = () => { - logger.log(`Removed indexeddb instance: ${this._dbName}`); + logger.log(`Removed indexeddb instance: ${this.dbName}`); resolve(); }; }).catch((e) => { @@ -193,8 +211,8 @@ export class IndexedDBCryptoStore { * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ - getOrAddOutgoingRoomKeyRequest(request) { - return this._backend.getOrAddOutgoingRoomKeyRequest(request); + public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise { + return this.backend.getOrAddOutgoingRoomKeyRequest(request); } /** @@ -207,8 +225,8 @@ export class IndexedDBCryptoStore { * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if * not found */ - getOutgoingRoomKeyRequest(requestBody) { - return this._backend.getOutgoingRoomKeyRequest(requestBody); + public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { + return this.backend.getOutgoingRoomKeyRequest(requestBody); } /** @@ -221,8 +239,8 @@ export class IndexedDBCryptoStore { * there are no pending requests in those states. If there are multiple * requests in those states, an arbitrary one is chosen. */ - getOutgoingRoomKeyRequestByState(wantedStates) { - return this._backend.getOutgoingRoomKeyRequestByState(wantedStates); + public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise { + return this.backend.getOutgoingRoomKeyRequestByState(wantedStates); } /** @@ -232,8 +250,8 @@ export class IndexedDBCryptoStore { * @param {Number} wantedState * @return {Promise>} Returns an array of requests in the given state */ - getAllOutgoingRoomKeyRequestsByState(wantedState) { - return this._backend.getAllOutgoingRoomKeyRequestsByState(wantedState); + public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise { + return this.backend.getAllOutgoingRoomKeyRequestsByState(wantedState); } /** @@ -246,8 +264,12 @@ export class IndexedDBCryptoStore { * @return {Promise} resolves to a list of all the * {@link module:crypto/store/base~OutgoingRoomKeyRequest} */ - getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { - return this._backend.getOutgoingRoomKeyRequestsByTarget( + public getOutgoingRoomKeyRequestsByTarget( + userId: string, + deviceId: string, + wantedStates: number[], + ): Promise { + return this.backend.getOutgoingRoomKeyRequestsByTarget( userId, deviceId, wantedStates, ); } @@ -264,8 +286,12 @@ export class IndexedDBCryptoStore { * {@link module:crypto/store/base~OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ - updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { - return this._backend.updateOutgoingRoomKeyRequest( + public updateOutgoingRoomKeyRequest( + requestId: string, + expectedState: number, + updates: Partial, + ): Promise { + return this.backend.updateOutgoingRoomKeyRequest( requestId, expectedState, updates, ); } @@ -279,8 +305,11 @@ export class IndexedDBCryptoStore { * * @returns {Promise} resolves once the operation is completed */ - deleteOutgoingRoomKeyRequest(requestId, expectedState) { - return this._backend.deleteOutgoingRoomKeyRequest(requestId, expectedState); + public deleteOutgoingRoomKeyRequest( + requestId: string, + expectedState: number, + ): Promise { + return this.backend.deleteOutgoingRoomKeyRequest(requestId, expectedState); } // Olm Account @@ -292,8 +321,8 @@ export class IndexedDBCryptoStore { * @param {*} txn An active transaction. See doTxn(). * @param {function(string)} func Called with the account pickle */ - getAccount(txn, func) { - this._backend.getAccount(txn, func); + public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void) { + this.backend.getAccount(txn, func); } /** @@ -301,10 +330,10 @@ export class IndexedDBCryptoStore { * This requires an active transaction. See doTxn(). * * @param {*} txn An active transaction. See doTxn(). - * @param {string} newData The new account pickle to store. + * @param {string} accountPickle The new account pickle to store. */ - storeAccount(txn, newData) { - this._backend.storeAccount(txn, newData); + public storeAccount(txn: IDBTransaction, accountPickle: string): void { + this.backend.storeAccount(txn, accountPickle); } /** @@ -315,8 +344,8 @@ export class IndexedDBCryptoStore { * @param {function(string)} func Called with the account keys object: * { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed */ - getCrossSigningKeys(txn, func) { - this._backend.getCrossSigningKeys(txn, func); + public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record) => void): void { + this.backend.getCrossSigningKeys(txn, func); } /** @@ -324,8 +353,12 @@ export class IndexedDBCryptoStore { * @param {function(string)} func Called with the private key * @param {string} type A key type */ - getSecretStorePrivateKey(txn, func, type) { - this._backend.getSecretStorePrivateKey(txn, func, type); + public getSecretStorePrivateKey( + txn: IDBTransaction, + func: (key: IEncryptedPayload | null) => void, + type: string, + ): void { + this.backend.getSecretStorePrivateKey(txn, func, type); } /** @@ -334,8 +367,8 @@ export class IndexedDBCryptoStore { * @param {*} txn An active transaction. See doTxn(). * @param {string} keys keys object as getCrossSigningKeys() */ - storeCrossSigningKeys(txn, keys) { - this._backend.storeCrossSigningKeys(txn, keys); + public storeCrossSigningKeys(txn: IDBTransaction, keys: Record): void { + this.backend.storeCrossSigningKeys(txn, keys); } /** @@ -345,8 +378,8 @@ export class IndexedDBCryptoStore { * @param {string} type The type of cross-signing private key to store * @param {string} key keys object as getCrossSigningKeys() */ - storeSecretStorePrivateKey(txn, type, key) { - this._backend.storeSecretStorePrivateKey(txn, type, key); + public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void { + this.backend.storeSecretStorePrivateKey(txn, type, key); } // Olm sessions @@ -356,8 +389,8 @@ export class IndexedDBCryptoStore { * @param {*} txn An active transaction. See doTxn(). * @param {function(int)} func Called with the count of sessions */ - countEndToEndSessions(txn, func) { - this._backend.countEndToEndSessions(txn, func); + public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void { + this.backend.countEndToEndSessions(txn, func); } /** @@ -372,8 +405,13 @@ export class IndexedDBCryptoStore { * timestamp in milliseconds at which the session last received * a message. */ - getEndToEndSession(deviceKey, sessionId, txn, func) { - this._backend.getEndToEndSession(deviceKey, sessionId, txn, func); + public getEndToEndSession( + deviceKey: string, + sessionId: string, + txn: IDBTransaction, + func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void, + ): void { + this.backend.getEndToEndSession(deviceKey, sessionId, txn, func); } /** @@ -387,8 +425,12 @@ export class IndexedDBCryptoStore { * timestamp in milliseconds at which the session last received * a message. */ - getEndToEndSessions(deviceKey, txn, func) { - this._backend.getEndToEndSessions(deviceKey, txn, func); + public getEndToEndSessions( + deviceKey: string, + txn: IDBTransaction, + func: (sessions: { [sessionId: string]: ISessionInfo }) => void, + ): void { + this.backend.getEndToEndSessions(deviceKey, txn, func); } /** @@ -398,8 +440,8 @@ export class IndexedDBCryptoStore { * an object with, deviceKey, lastReceivedMessageTs, sessionId * and session keys. */ - getAllEndToEndSessions(txn, func) { - this._backend.getAllEndToEndSessions(txn, func); + public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void { + this.backend.getAllEndToEndSessions(txn, func); } /** @@ -409,22 +451,25 @@ export class IndexedDBCryptoStore { * @param {string} sessionInfo Session information object * @param {*} txn An active transaction. See doTxn(). */ - storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { - this._backend.storeEndToEndSession( - deviceKey, sessionId, sessionInfo, txn, - ); + public storeEndToEndSession( + deviceKey: string, + sessionId: string, + sessionInfo: ISessionInfo, + txn: IDBTransaction, + ): void { + this.backend.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn); } - storeEndToEndSessionProblem(deviceKey, type, fixed) { - return this._backend.storeEndToEndSessionProblem(deviceKey, type, fixed); + public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise { + return this.backend.storeEndToEndSessionProblem(deviceKey, type, fixed); } - getEndToEndSessionProblem(deviceKey, timestamp) { - return this._backend.getEndToEndSessionProblem(deviceKey, timestamp); + public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise { + return this.backend.getEndToEndSessionProblem(deviceKey, timestamp); } - filterOutNotifiedErrorDevices(devices) { - return this._backend.filterOutNotifiedErrorDevices(devices); + public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise { + return this.backend.filterOutNotifiedErrorDevices(devices); } // Inbound group sessions @@ -438,10 +483,13 @@ export class IndexedDBCryptoStore { * @param {function(object)} func Called with A map from sessionId * to Base64 end-to-end session. */ - getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { - this._backend.getEndToEndInboundGroupSession( - senderCurve25519Key, sessionId, txn, func, - ); + public getEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + txn: IDBTransaction, + func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void, + ): void { + this.backend.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func); } /** @@ -451,8 +499,11 @@ export class IndexedDBCryptoStore { * in the store with an object having keys {senderKey, sessionId, * sessionData}, then once with null to indicate the end of the list. */ - getAllEndToEndInboundGroupSessions(txn, func) { - this._backend.getAllEndToEndInboundGroupSessions(txn, func); + public getAllEndToEndInboundGroupSessions( + txn: IDBTransaction, + func: (session: ISession | null) => void, + ): void { + this.backend.getAllEndToEndInboundGroupSessions(txn, func); } /** @@ -464,10 +515,13 @@ export class IndexedDBCryptoStore { * @param {object} sessionData The session data structure * @param {*} txn An active transaction. See doTxn(). */ - addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { - this._backend.addEndToEndInboundGroupSession( - senderCurve25519Key, sessionId, sessionData, txn, - ); + public addEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + sessionData: InboundGroupSessionData, + txn: IDBTransaction, + ): void { + this.backend.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); } /** @@ -479,18 +533,22 @@ export class IndexedDBCryptoStore { * @param {object} sessionData The session data structure * @param {*} txn An active transaction. See doTxn(). */ - storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { - this._backend.storeEndToEndInboundGroupSession( - senderCurve25519Key, sessionId, sessionData, txn, - ); + public storeEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + sessionData: InboundGroupSessionData, + txn: IDBTransaction, + ): void { + this.backend.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); } - storeEndToEndInboundGroupSessionWithheld( - senderCurve25519Key, sessionId, sessionData, txn, - ) { - this._backend.storeEndToEndInboundGroupSessionWithheld( - senderCurve25519Key, sessionId, sessionData, txn, - ); + public storeEndToEndInboundGroupSessionWithheld( + senderCurve25519Key: string, + sessionId: string, + sessionData: IWithheld, + txn: IDBTransaction, + ): void { + this.backend.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn); } // End-to-end device tracking @@ -505,8 +563,8 @@ export class IndexedDBCryptoStore { * @param {Object} deviceData * @param {*} txn An active transaction. See doTxn(). */ - storeEndToEndDeviceData(deviceData, txn) { - this._backend.storeEndToEndDeviceData(deviceData, txn); + public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void { + this.backend.storeEndToEndDeviceData(deviceData, txn); } /** @@ -516,8 +574,8 @@ export class IndexedDBCryptoStore { * @param {function(Object)} func Function called with the * device data */ - getEndToEndDeviceData(txn, func) { - this._backend.getEndToEndDeviceData(txn, func); + public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void { + this.backend.getEndToEndDeviceData(txn, func); } // End to End Rooms @@ -528,8 +586,8 @@ export class IndexedDBCryptoStore { * @param {object} roomInfo The end-to-end info for the room. * @param {*} txn An active transaction. See doTxn(). */ - storeEndToEndRoom(roomId, roomInfo, txn) { - this._backend.storeEndToEndRoom(roomId, roomInfo, txn); + public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void { + this.backend.storeEndToEndRoom(roomId, roomInfo, txn); } /** @@ -537,20 +595,20 @@ export class IndexedDBCryptoStore { * @param {*} txn An active transaction. See doTxn(). * @param {function(Object)} func Function called with the end to end encrypted rooms */ - getEndToEndRooms(txn, func) { - this._backend.getEndToEndRooms(txn, func); + public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record) => void): void { + this.backend.getEndToEndRooms(txn, func); } // session backups /** * Get the inbound group sessions that need to be backed up. - * @param {integer} limit The maximum number of sessions to retrieve. 0 + * @param {number} limit The maximum number of sessions to retrieve. 0 * for no limit. * @returns {Promise} resolves to an array of inbound group sessions */ - getSessionsNeedingBackup(limit) { - return this._backend.getSessionsNeedingBackup(limit); + public getSessionsNeedingBackup(limit: number): Promise { + return this.backend.getSessionsNeedingBackup(limit); } /** @@ -558,8 +616,8 @@ export class IndexedDBCryptoStore { * @param {*} txn An active transaction. See doTxn(). (optional) * @returns {Promise} resolves to the number of sessions */ - countSessionsNeedingBackup(txn) { - return this._backend.countSessionsNeedingBackup(txn); + public countSessionsNeedingBackup(txn?: IDBTransaction): Promise { + return this.backend.countSessionsNeedingBackup(txn); } /** @@ -568,8 +626,8 @@ export class IndexedDBCryptoStore { * @param {*} txn An active transaction. See doTxn(). (optional) * @returns {Promise} resolves when the sessions are unmarked */ - unmarkSessionsNeedingBackup(sessions, txn) { - return this._backend.unmarkSessionsNeedingBackup(sessions, txn); + public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise { + return this.backend.unmarkSessionsNeedingBackup(sessions, txn); } /** @@ -578,8 +636,8 @@ export class IndexedDBCryptoStore { * @param {*} txn An active transaction. See doTxn(). (optional) * @returns {Promise} resolves when the sessions are marked */ - markSessionsNeedingBackup(sessions, txn) { - return this._backend.markSessionsNeedingBackup(sessions, txn); + public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise { + return this.backend.markSessionsNeedingBackup(sessions, txn); } /** @@ -589,10 +647,13 @@ export class IndexedDBCryptoStore { * @param {string} sessionId The ID of the session * @param {*} txn An active transaction. See doTxn(). (optional) */ - addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) { - this._backend.addSharedHistoryInboundGroupSession( - roomId, senderKey, sessionId, txn, - ); + public addSharedHistoryInboundGroupSession( + roomId: string, + senderKey: string, + sessionId: string, + txn?: IDBTransaction, + ): void { + this.backend.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn); } /** @@ -601,8 +662,11 @@ export class IndexedDBCryptoStore { * @param {*} txn An active transaction. See doTxn(). (optional) * @returns {Promise} Resolves to an array of [senderKey, sessionId] */ - getSharedHistoryInboundGroupSessions(roomId, txn) { - return this._backend.getSharedHistoryInboundGroupSessions(roomId, txn); + public getSharedHistoryInboundGroupSessions( + roomId: string, + txn?: IDBTransaction, + ): Promise<[senderKey: string, sessionId: string][]> { + return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn); } /** @@ -627,18 +691,7 @@ export class IndexedDBCryptoStore { * reject with that exception. On synchronous backends, the * exception will propagate to the caller of the getFoo method. */ - doTxn(mode, stores, func, log) { - return this._backend.doTxn(mode, stores, func, log); + doTxn(mode: Mode, stores: Iterable, func: (txn: IDBTransaction) => T, log?: PrefixedLogger): Promise { + return this.backend.doTxn(mode, stores, func, log); } } - -IndexedDBCryptoStore.STORE_ACCOUNT = 'account'; -IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; -IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; -IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD - = 'inbound_group_sessions_withheld'; -IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS - = 'shared_history_inbound_group_sessions'; -IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; -IndexedDBCryptoStore.STORE_ROOMS = 'rooms'; -IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup'; diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.ts similarity index 62% rename from src/crypto/store/localStorage-crypto-store.js rename to src/crypto/store/localStorage-crypto-store.ts index 0a982311e..6e0492191 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.ts @@ -1,6 +1,5 @@ /* -Copyright 2017, 2018 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2017 - 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. @@ -17,6 +16,12 @@ limitations under the License. import { logger } from '../../logger'; import { MemoryCryptoStore } from './memory-crypto-store'; +import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode } from "./base"; +import { IOlmDevice } from "../algorithms/megolm"; +import { IRoomEncryption } from "../RoomList"; +import { ICrossSigningKey } from "../../client"; +import { InboundGroupSessionData } from "../../@types/partials"; +import { IEncryptedPayload } from "../aes"; /** * Internal module. Partial localStorage backed storage for e2e. @@ -38,23 +43,23 @@ const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.w const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup"; -function keyEndToEndSessions(deviceKey) { +function keyEndToEndSessions(deviceKey: string): string { return E2E_PREFIX + "sessions/" + deviceKey; } -function keyEndToEndSessionProblems(deviceKey) { +function keyEndToEndSessionProblems(deviceKey: string): string { return E2E_PREFIX + "session.problems/" + deviceKey; } -function keyEndToEndInboundGroupSession(senderKey, sessionId) { +function keyEndToEndInboundGroupSession(senderKey: string, sessionId: string): string { return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; } -function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) { +function keyEndToEndInboundGroupSessionWithheld(senderKey: string, sessionId: string): string { return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId; } -function keyEndToEndRoomsPrefix(roomId) { +function keyEndToEndRoomsPrefix(roomId: string): string { return KEY_ROOMS_PREFIX + roomId; } @@ -62,24 +67,23 @@ function keyEndToEndRoomsPrefix(roomId) { * @implements {module:crypto/store/base~CryptoStore} */ export class LocalStorageCryptoStore extends MemoryCryptoStore { - constructor(webStore) { - super(); - this.store = webStore; - } - - static exists(webStore) { - const length = webStore.length; + public static exists(store: Storage): boolean { + const length = store.length; for (let i = 0; i < length; i++) { - if (webStore.key(i).startsWith(E2E_PREFIX)) { + if (store.key(i).startsWith(E2E_PREFIX)) { return true; } } return false; } + constructor(private readonly store: Storage) { + super(); + } + // Olm Sessions - countEndToEndSessions(txn, func) { + public countEndToEndSessions(txn: unknown, func: (count: number) => void): void { let count = 0; for (let i = 0; i < this.store.length; ++i) { if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count; @@ -87,9 +91,10 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { func(count); } - _getEndToEndSessions(deviceKey, txn, func) { + // eslint-disable-next-line @typescript-eslint/naming-convention + private _getEndToEndSessions(deviceKey: string): Record { const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey)); - const fixedSessions = {}; + const fixedSessions: Record = {}; // fix up any old sessions to be objects rather than just the base64 pickle for (const [sid, val] of Object.entries(sessions || {})) { @@ -105,16 +110,25 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { return fixedSessions; } - getEndToEndSession(deviceKey, sessionId, txn, func) { + public getEndToEndSession( + deviceKey: string, + sessionId: string, + txn: unknown, + func: (session: ISessionInfo) => void, + ): void { const sessions = this._getEndToEndSessions(deviceKey); func(sessions[sessionId] || {}); } - getEndToEndSessions(deviceKey, txn, func) { + public getEndToEndSessions( + deviceKey: string, + txn: unknown, + func: (sessions: { [sessionId: string]: ISessionInfo }) => void, + ): void { func(this._getEndToEndSessions(deviceKey) || {}); } - getAllEndToEndSessions(txn, func) { + public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void { for (let i = 0; i < this.store.length; ++i) { if (this.store.key(i).startsWith(keyEndToEndSessions(''))) { const deviceKey = this.store.key(i).split('/')[1]; @@ -125,17 +139,15 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { } } - storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { + public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void { const sessions = this._getEndToEndSessions(deviceKey) || {}; sessions[sessionId] = sessionInfo; - setJsonItem( - this.store, keyEndToEndSessions(deviceKey), sessions, - ); + setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions); } - async storeEndToEndSessionProblem(deviceKey, type, fixed) { + public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise { const key = keyEndToEndSessionProblems(deviceKey); - const problems = getJsonItem(this.store, key) || []; + const problems = getJsonItem(this.store, key) || []; problems.push({ type, fixed, time: Date.now() }); problems.sort((a, b) => { return a.time - b.time; @@ -143,9 +155,9 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { setJsonItem(this.store, key, problems); } - async getEndToEndSessionProblem(deviceKey, timestamp) { + async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise { const key = keyEndToEndSessionProblems(deviceKey); - const problems = getJsonItem(this.store, key) || []; + const problems = getJsonItem(this.store, key) || []; if (!problems.length) { return null; } @@ -162,9 +174,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { } } - async filterOutNotifiedErrorDevices(devices) { - const notifiedErrorDevices = - getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {}; + public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise { + const notifiedErrorDevices = getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {}; const ret = []; for (const device of devices) { @@ -187,7 +198,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { // Inbound Group Sessions - getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + public getEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + txn: unknown, + func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void, + ): void { func( getJsonItem( this.store, @@ -200,7 +216,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { ); } - getAllEndToEndInboundGroupSessions(txn, func) { + public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void { for (let i = 0; i < this.store.length; ++i) { const key = this.store.key(i); if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) { @@ -219,7 +235,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { func(null); } - addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + public addEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + sessionData: InboundGroupSessionData, + txn: unknown, + ): void { const existing = getJsonItem( this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), @@ -231,7 +252,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { } } - storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + public storeEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + sessionData: InboundGroupSessionData, + txn: unknown, + ): void { setJsonItem( this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), @@ -239,9 +265,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { ); } - storeEndToEndInboundGroupSessionWithheld( - senderCurve25519Key, sessionId, sessionData, txn, - ) { + public storeEndToEndInboundGroupSessionWithheld( + senderCurve25519Key: string, + sessionId: string, + sessionData: IWithheld, + txn: unknown, + ): void { setJsonItem( this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId), @@ -249,25 +278,19 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { ); } - getEndToEndDeviceData(txn, func) { - func(getJsonItem( - this.store, KEY_DEVICE_DATA, - )); + public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void { + func(getJsonItem(this.store, KEY_DEVICE_DATA)); } - storeEndToEndDeviceData(deviceData, txn) { - setJsonItem( - this.store, KEY_DEVICE_DATA, deviceData, - ); + public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void { + setJsonItem(this.store, KEY_DEVICE_DATA, deviceData); } - storeEndToEndRoom(roomId, roomInfo, txn) { - setJsonItem( - this.store, keyEndToEndRoomsPrefix(roomId), roomInfo, - ); + public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void { + setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo); } - getEndToEndRooms(txn, func) { + public getEndToEndRooms(txn: unknown, func: (rooms: Record) => void): void { const result = {}; const prefix = keyEndToEndRoomsPrefix(''); @@ -281,9 +304,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { func(result); } - getSessionsNeedingBackup(limit) { - const sessionsNeedingBackup - = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + public getSessionsNeedingBackup(limit: number): Promise { + const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; const sessions = []; for (const session in sessionsNeedingBackup) { @@ -309,13 +331,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { return Promise.resolve(sessions); } - countSessionsNeedingBackup() { - const sessionsNeedingBackup - = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + public countSessionsNeedingBackup(): Promise { + const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; return Promise.resolve(Object.keys(sessionsNeedingBackup).length); } - unmarkSessionsNeedingBackup(sessions) { + public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise { const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; for (const session of sessions) { @@ -327,7 +348,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { return Promise.resolve(); } - markSessionsNeedingBackup(sessions) { + public markSessionsNeedingBackup(sessions: ISession[]): Promise { const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; for (const session of sessions) { @@ -344,52 +365,46 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { * * @returns {Promise} Promise which resolves when the store has been cleared. */ - deleteAllData() { + public deleteAllData(): Promise { this.store.removeItem(KEY_END_TO_END_ACCOUNT); return Promise.resolve(); } // Olm account - getAccount(txn, func) { - const account = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT); - func(account); + public getAccount(txn: unknown, func: (accountPickle: string) => void): void { + const accountPickle = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT); + func(accountPickle); } - storeAccount(txn, newData) { - setJsonItem( - this.store, KEY_END_TO_END_ACCOUNT, newData, - ); + public storeAccount(txn: unknown, accountPickle: string): void { + setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle); } - getCrossSigningKeys(txn, func) { - const keys = getJsonItem(this.store, KEY_CROSS_SIGNING_KEYS); + public getCrossSigningKeys(txn: unknown, func: (keys: Record) => void): void { + const keys = getJsonItem>(this.store, KEY_CROSS_SIGNING_KEYS); func(keys); } - getSecretStorePrivateKey(txn, func, type) { - const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`); + public getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void { + const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`); func(key); } - storeCrossSigningKeys(txn, keys) { - setJsonItem( - this.store, KEY_CROSS_SIGNING_KEYS, keys, - ); + public storeCrossSigningKeys(txn: unknown, keys: Record): void { + setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys); } - storeSecretStorePrivateKey(txn, type, key) { - setJsonItem( - this.store, E2E_PREFIX + `ssss_cache.${type}`, key, - ); + public storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void { + setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key); } - doTxn(mode, stores, func) { + doTxn(mode: Mode, stores: Iterable, func: (txn: unknown) => T): Promise { return Promise.resolve(func(null)); } } -function getJsonItem(store, key) { +function getJsonItem(store: Storage, key: string): T | null { try { // if the key is absent, store.getItem() returns null, and // JSON.parse(null) === null, so this returns null. @@ -401,6 +416,6 @@ function getJsonItem(store, key) { return null; } -function setJsonItem(store, key, val) { +function setJsonItem(store: Storage, key: string, val: T): void { store.setItem(key, JSON.stringify(val)); } diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.ts similarity index 51% rename from src/crypto/store/memory-crypto-store.js rename to src/crypto/store/memory-crypto-store.ts index 9577fcad3..6668f7a6d 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.ts @@ -1,7 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2017 - 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. @@ -18,6 +16,22 @@ limitations under the License. import { logger } from '../../logger'; import * as utils from "../../utils"; +import { + CryptoStore, + IDeviceData, + IProblem, + ISession, + ISessionInfo, + IWithheld, + Mode, + OutgoingRoomKeyRequest, +} from "./base"; +import { IRoomKeyRequestBody } from "../index"; +import { ICrossSigningKey } from "../../client"; +import { IOlmDevice } from "../algorithms/megolm"; +import { IRoomEncryption } from "../RoomList"; +import { InboundGroupSessionData } from "../../@types/partials"; +import { IEncryptedPayload } from "../aes"; /** * Internal module. in-memory storage for e2e. @@ -28,32 +42,22 @@ import * as utils from "../../utils"; /** * @implements {module:crypto/store/base~CryptoStore} */ -export class MemoryCryptoStore { - constructor() { - this._outgoingRoomKeyRequests = []; - this._account = null; - this._crossSigningKeys = null; - this._privateKeys = {}; - this._backupKeys = {}; +export class MemoryCryptoStore implements CryptoStore { + private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = []; + private account: string = null; + private crossSigningKeys: Record = null; + private privateKeys: Record = {}; - // Map of {devicekey -> {sessionId -> session pickle}} - this._sessions = {}; - // Map of {devicekey -> array of problems} - this._sessionProblems = {}; - // Map of {userId -> deviceId -> true} - this._notifiedErrorDevices = {}; - // Map of {senderCurve25519Key+'/'+sessionId -> session data object} - this._inboundGroupSessions = {}; - this._inboundGroupSessionsWithheld = {}; - // Opaque device data object - this._deviceData = null; - // roomId -> Opaque roomInfo object - this._rooms = {}; - // Set of {senderCurve25519Key+'/'+sessionId} - this._sessionsNeedingBackup = {}; - // roomId -> array of [senderKey, sessionId] - this._sharedHistoryInboundGroupSessions = {}; - } + private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {}; + private sessionProblems: { [deviceKey: string]: IProblem[] } = {}; + private notifiedErrorDevices: { [userId: string]: { [deviceId: string]: boolean } } = {}; + private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {}; + private inboundGroupSessionsWithheld: Record = {}; + // Opaque device data object + private deviceData: IDeviceData = null; + private rooms: { [roomId: string]: IRoomEncryption } = {}; + private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {}; + private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {}; /** * Ensure the database exists and is up-to-date. @@ -62,7 +66,7 @@ export class MemoryCryptoStore { * * @return {Promise} resolves to the store. */ - async startup() { + public async startup(): Promise { // No startup work to do for the memory store. return this; } @@ -72,7 +76,7 @@ export class MemoryCryptoStore { * * @returns {Promise} Promise which resolves when the store has been cleared. */ - deleteAllData() { + public deleteAllData(): Promise { return Promise.resolve(); } @@ -86,7 +90,7 @@ export class MemoryCryptoStore { * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ - getOrAddOutgoingRoomKeyRequest(request) { + public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise { const requestBody = request.requestBody; return utils.promiseTry(() => { @@ -109,7 +113,7 @@ export class MemoryCryptoStore { `enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id, ); - this._outgoingRoomKeyRequests.push(request); + this.outgoingRoomKeyRequests.push(request); return request; }); } @@ -124,7 +128,7 @@ export class MemoryCryptoStore { * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if * not found */ - getOutgoingRoomKeyRequest(requestBody) { + public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody)); } @@ -139,8 +143,9 @@ export class MemoryCryptoStore { * @return {module:crypto/store/base~OutgoingRoomKeyRequest?} * the matching request, or null if not found */ - _getOutgoingRoomKeyRequest(requestBody) { - for (const existing of this._outgoingRoomKeyRequests) { + // eslint-disable-next-line @typescript-eslint/naming-convention + private _getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): OutgoingRoomKeyRequest | null { + for (const existing of this.outgoingRoomKeyRequests) { if (utils.deepCompare(existing.requestBody, requestBody)) { return existing; } @@ -157,8 +162,8 @@ export class MemoryCryptoStore { * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if * there are no pending requests in those states */ - getOutgoingRoomKeyRequestByState(wantedStates) { - for (const req of this._outgoingRoomKeyRequests) { + public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise { + for (const req of this.outgoingRoomKeyRequests) { for (const state of wantedStates) { if (req.state === state) { return Promise.resolve(req); @@ -173,18 +178,22 @@ export class MemoryCryptoStore { * @param {Number} wantedState * @return {Promise>} All OutgoingRoomKeyRequests in state */ - getAllOutgoingRoomKeyRequestsByState(wantedState) { + public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise { return Promise.resolve( - this._outgoingRoomKeyRequests.filter( + this.outgoingRoomKeyRequests.filter( (r) => r.state == wantedState, ), ); } - getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { + public getOutgoingRoomKeyRequestsByTarget( + userId: string, + deviceId: string, + wantedStates: number[], + ): Promise { const results = []; - for (const req of this._outgoingRoomKeyRequests) { + for (const req of this.outgoingRoomKeyRequests) { for (const state of wantedStates) { if (req.state === state && req.recipients.includes({ userId, deviceId })) { results.push(req); @@ -206,13 +215,17 @@ export class MemoryCryptoStore { * {@link module:crypto/store/base~OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ - updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { - for (const req of this._outgoingRoomKeyRequests) { + public updateOutgoingRoomKeyRequest( + requestId: string, + expectedState: number, + updates: Partial, + ): Promise { + for (const req of this.outgoingRoomKeyRequests) { if (req.requestId !== requestId) { continue; } - if (req.state != expectedState) { + if (req.state !== expectedState) { logger.warn( `Cannot update room key request from ${expectedState} ` + `as it was already updated to ${req.state}`, @@ -235,9 +248,12 @@ export class MemoryCryptoStore { * * @returns {Promise} resolves once the operation is completed */ - deleteOutgoingRoomKeyRequest(requestId, expectedState) { - for (let i = 0; i < this._outgoingRoomKeyRequests.length; i++) { - const req = this._outgoingRoomKeyRequests[i]; + public deleteOutgoingRoomKeyRequest( + requestId: string, + expectedState: number, + ): Promise { + for (let i = 0; i < this.outgoingRoomKeyRequests.length; i++) { + const req = this.outgoingRoomKeyRequests[i]; if (req.requestId !== requestId) { continue; @@ -251,7 +267,7 @@ export class MemoryCryptoStore { return Promise.resolve(null); } - this._outgoingRoomKeyRequests.splice(i, 1); + this.outgoingRoomKeyRequests.splice(i, 1); return Promise.resolve(req); } @@ -260,48 +276,57 @@ export class MemoryCryptoStore { // Olm Account - getAccount(txn, func) { - func(this._account); + public getAccount(txn: unknown, func: (accountPickle: string) => void) { + func(this.account); } - storeAccount(txn, newData) { - this._account = newData; + public storeAccount(txn: unknown, accountPickle: string): void { + this.account = accountPickle; } - getCrossSigningKeys(txn, func) { - func(this._crossSigningKeys); + public getCrossSigningKeys(txn: unknown, func: (keys: Record) => void): void { + func(this.crossSigningKeys); } - getSecretStorePrivateKey(txn, func, type) { - const result = this._privateKeys[type]; - return func(result || null); + public getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void { + const result = this.privateKeys[type]; + func(result || null); } - storeCrossSigningKeys(txn, keys) { - this._crossSigningKeys = keys; + public storeCrossSigningKeys(txn: unknown, keys: Record): void { + this.crossSigningKeys = keys; } - storeSecretStorePrivateKey(txn, type, key) { - this._privateKeys[type] = key; + public storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void { + this.privateKeys[type] = key; } // Olm Sessions - countEndToEndSessions(txn, func) { - return Object.keys(this._sessions).length; + public countEndToEndSessions(txn: unknown, func: (count: number) => void): void { + func(Object.keys(this.sessions).length); } - getEndToEndSession(deviceKey, sessionId, txn, func) { - const deviceSessions = this._sessions[deviceKey] || {}; + public getEndToEndSession( + deviceKey: string, + sessionId: string, + txn: unknown, + func: (session: ISessionInfo) => void, + ): void { + const deviceSessions = this.sessions[deviceKey] || {}; func(deviceSessions[sessionId] || null); } - getEndToEndSessions(deviceKey, txn, func) { - func(this._sessions[deviceKey] || {}); + public getEndToEndSessions( + deviceKey: string, + txn: unknown, + func: (sessions: { [sessionId: string]: ISessionInfo }) => void, + ): void { + func(this.sessions[deviceKey] || {}); } - getAllEndToEndSessions(txn, func) { - Object.entries(this._sessions).forEach(([deviceKey, deviceSessions]) => { + public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void { + Object.entries(this.sessions).forEach(([deviceKey, deviceSessions]) => { Object.entries(deviceSessions).forEach(([sessionId, session]) => { func({ ...session, @@ -312,26 +337,25 @@ export class MemoryCryptoStore { }); } - storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { - let deviceSessions = this._sessions[deviceKey]; + public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void { + let deviceSessions = this.sessions[deviceKey]; if (deviceSessions === undefined) { deviceSessions = {}; - this._sessions[deviceKey] = deviceSessions; + this.sessions[deviceKey] = deviceSessions; } deviceSessions[sessionId] = sessionInfo; } - async storeEndToEndSessionProblem(deviceKey, type, fixed) { - const problems = this._sessionProblems[deviceKey] - = this._sessionProblems[deviceKey] || []; + public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise { + const problems = this.sessionProblems[deviceKey] = this.sessionProblems[deviceKey] || []; problems.push({ type, fixed, time: Date.now() }); problems.sort((a, b) => { return a.time - b.time; }); } - async getEndToEndSessionProblem(deviceKey, timestamp) { - const problems = this._sessionProblems[deviceKey] || []; + public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise { + const problems = this.sessionProblems[deviceKey] || []; if (!problems.length) { return null; } @@ -348,9 +372,9 @@ export class MemoryCryptoStore { } } - async filterOutNotifiedErrorDevices(devices) { - const notifiedErrorDevices = this._notifiedErrorDevices; - const ret = []; + public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise { + const notifiedErrorDevices = this.notifiedErrorDevices; + const ret: IOlmDevice[] = []; for (const device of devices) { const { userId, deviceInfo } = device; @@ -370,16 +394,24 @@ export class MemoryCryptoStore { // Inbound Group Sessions - getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + public getEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + txn: unknown, + func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void, + ): void { const k = senderCurve25519Key+'/'+sessionId; func( - this._inboundGroupSessions[k] || null, - this._inboundGroupSessionsWithheld[k] || null, + this.inboundGroupSessions[k] || null, + this.inboundGroupSessionsWithheld[k] || null, ); } - getAllEndToEndInboundGroupSessions(txn, func) { - for (const key of Object.keys(this._inboundGroupSessions)) { + public getAllEndToEndInboundGroupSessions( + txn: unknown, + func: (session: ISession | null) => void, + ): void { + for (const key of Object.keys(this.inboundGroupSessions)) { // we can't use split, as the components we are trying to split out // might themselves contain '/' characters. We rely on the // senderKey being a (32-byte) curve25519 key, base64-encoded @@ -388,58 +420,71 @@ export class MemoryCryptoStore { func({ senderKey: key.substr(0, 43), sessionId: key.substr(44), - sessionData: this._inboundGroupSessions[key], + sessionData: this.inboundGroupSessions[key], }); } func(null); } - addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + public addEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + sessionData: InboundGroupSessionData, + txn: unknown, + ): void { const k = senderCurve25519Key+'/'+sessionId; - if (this._inboundGroupSessions[k] === undefined) { - this._inboundGroupSessions[k] = sessionData; + if (this.inboundGroupSessions[k] === undefined) { + this.inboundGroupSessions[k] = sessionData; } } - storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { - this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData; + public storeEndToEndInboundGroupSession( + senderCurve25519Key: string, + sessionId: string, + sessionData: InboundGroupSessionData, + txn: unknown, + ): void { + this.inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData; } - storeEndToEndInboundGroupSessionWithheld( - senderCurve25519Key, sessionId, sessionData, txn, - ) { + public storeEndToEndInboundGroupSessionWithheld( + senderCurve25519Key: string, + sessionId: string, + sessionData: IWithheld, + txn: unknown, + ): void { const k = senderCurve25519Key+'/'+sessionId; - this._inboundGroupSessionsWithheld[k] = sessionData; + this.inboundGroupSessionsWithheld[k] = sessionData; } // Device Data - getEndToEndDeviceData(txn, func) { - func(this._deviceData); + public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void { + func(this.deviceData); } - storeEndToEndDeviceData(deviceData, txn) { - this._deviceData = deviceData; + public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void { + this.deviceData = deviceData; } // E2E rooms - storeEndToEndRoom(roomId, roomInfo, txn) { - this._rooms[roomId] = roomInfo; + public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void { + this.rooms[roomId] = roomInfo; } - getEndToEndRooms(txn, func) { - func(this._rooms); + public getEndToEndRooms(txn: unknown, func: (rooms: Record) => void): void { + func(this.rooms); } - getSessionsNeedingBackup(limit) { - const sessions = []; - for (const session in this._sessionsNeedingBackup) { - if (this._inboundGroupSessions[session]) { + public getSessionsNeedingBackup(limit: number): Promise { + const sessions: ISession[] = []; + for (const session in this.sessionsNeedingBackup) { + if (this.inboundGroupSessions[session]) { sessions.push({ senderKey: session.substr(0, 43), sessionId: session.substr(44), - sessionData: this._inboundGroupSessions[session], + sessionData: this.inboundGroupSessions[session], }); if (limit && session.length >= limit) { break; @@ -449,39 +494,39 @@ export class MemoryCryptoStore { return Promise.resolve(sessions); } - countSessionsNeedingBackup() { - return Promise.resolve(Object.keys(this._sessionsNeedingBackup).length); + public countSessionsNeedingBackup(): Promise { + return Promise.resolve(Object.keys(this.sessionsNeedingBackup).length); } - unmarkSessionsNeedingBackup(sessions) { + public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise { for (const session of sessions) { const sessionKey = session.senderKey + '/' + session.sessionId; - delete this._sessionsNeedingBackup[sessionKey]; + delete this.sessionsNeedingBackup[sessionKey]; } return Promise.resolve(); } - markSessionsNeedingBackup(sessions) { + public markSessionsNeedingBackup(sessions: ISession[]): Promise { for (const session of sessions) { const sessionKey = session.senderKey + '/' + session.sessionId; - this._sessionsNeedingBackup[sessionKey] = true; + this.sessionsNeedingBackup[sessionKey] = true; } return Promise.resolve(); } - addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId) { - const sessions = this._sharedHistoryInboundGroupSessions[roomId] || []; + public addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string): void { + const sessions = this.sharedHistoryInboundGroupSessions[roomId] || []; sessions.push([senderKey, sessionId]); - this._sharedHistoryInboundGroupSessions[roomId] = sessions; + this.sharedHistoryInboundGroupSessions[roomId] = sessions; } - getSharedHistoryInboundGroupSessions(roomId) { - return Promise.resolve(this._sharedHistoryInboundGroupSessions[roomId] || []); + public getSharedHistoryInboundGroupSessions(roomId: string): Promise<[senderKey: string, sessionId: string][]> { + return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []); } // Session key backups - doTxn(mode, stores, func) { + public doTxn(mode: Mode, stores: Iterable, func: (txn?: unknown) => T): Promise { return Promise.resolve(func(null)); } } diff --git a/src/indexeddb-helpers.js b/src/indexeddb-helpers.ts similarity index 88% rename from src/indexeddb-helpers.js rename to src/indexeddb-helpers.ts index 01123338d..84de7c0e9 100644 --- a/src/indexeddb-helpers.js +++ b/src/indexeddb-helpers.ts @@ -22,8 +22,8 @@ limitations under the License. * @param {string} dbName The database name to test for * @returns {boolean} Whether the database exists */ -export function exists(indexedDB, dbName) { - return new Promise((resolve, reject) => { +export function exists(indexedDB: IDBFactory, dbName: string): Promise { + return new Promise((resolve, reject) => { let exists = true; const req = indexedDB.open(dbName); req.onupgradeneeded = () => { @@ -31,7 +31,7 @@ export function exists(indexedDB, dbName) { // should only fire if the DB did not exist before at any version. exists = false; }; - req.onblocked = () => reject(); + req.onblocked = () => reject(req.error); req.onsuccess = () => { const db = req.result; db.close(); @@ -45,6 +45,6 @@ export function exists(indexedDB, dbName) { } resolve(exists); }; - req.onerror = ev => reject(ev.target.error); + req.onerror = ev => reject(req.error); }); } diff --git a/src/logger.ts b/src/logger.ts index bd1f98b25..d80fb2048 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -62,7 +62,7 @@ log.methodFactory = function(methodName, logLevel, loggerName) { export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE); logger.setLevel(log.levels.DEBUG); -interface PrefixedLogger extends Logger { +export interface PrefixedLogger extends Logger { withPrefix?: (prefix: string) => PrefixedLogger; prefix?: string; } diff --git a/src/matrix.ts b/src/matrix.ts index 78906a716..97709e314 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -154,7 +154,7 @@ export function createClient(opts: ICreateClientOpts | string) { } opts.request = opts.request || requestInstance; opts.store = opts.store || new MemoryStore({ - localStorage: global.localStorage, + localStorage: global.localStorage, }); opts.scheduler = opts.scheduler || new MatrixScheduler(); opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory(); diff --git a/src/models/MSC3089Branch.ts b/src/models/MSC3089Branch.ts index 7bb8c356e..875f9bd7d 100644 --- a/src/models/MSC3089Branch.ts +++ b/src/models/MSC3089Branch.ts @@ -82,6 +82,19 @@ export class MSC3089Branch { * @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file. */ public async getFileInfo(): Promise<{ info: IEncryptedFile, httpUrl: string }> { + const event = await this.getFileEvent(); + + const file = event.getContent()['file']; + const httpUrl = this.client.mxcUrlToHttp(file['url']); + + return { info: file, httpUrl: httpUrl }; + } + + /** + * Gets the event the file points to. + * @returns {Promise} Resolves to the file's event. + */ + public async getFileEvent(): Promise { const room = this.client.getRoom(this.roomId); if (!room) throw new Error("Unknown room"); @@ -94,9 +107,6 @@ export class MSC3089Branch { // Sometimes the event context doesn't decrypt for us, so do that. await this.client.decryptEventIfNeeded(event, { emit: false, isRetry: false }); - const file = event.getContent()['file']; - const httpUrl = this.client.mxcUrlToHttp(file['url']); - - return { info: file, httpUrl: httpUrl }; + return event; } } diff --git a/src/models/event.ts b/src/models/event.ts index 2c5655717..16383c9ae 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -153,7 +153,7 @@ export class MatrixEvent extends EventEmitter { private _replacingEvent: MatrixEvent = null; private _localRedactionEvent: MatrixEvent = null; private _isCancelled = false; - private clearEvent: Partial = {}; + private clearEvent?: IClearEvent; /* curve25519 key which we believe belongs to the sender of the event. See * getSenderKey() @@ -296,7 +296,10 @@ export class MatrixEvent extends EventEmitter { * @return {string} The event type, e.g. m.room.message */ public getType(): EventType | string { - return this.clearEvent.type || this.event.type; + if (this.clearEvent) { + return this.clearEvent.type; + } + return this.event.type; } /** @@ -345,7 +348,10 @@ export class MatrixEvent extends EventEmitter { if (this._localRedactionEvent) { return {} as T; } - return (this.clearEvent.content || this.event.content || {}) as T; + if (this.clearEvent) { + return (this.clearEvent.content || {}) as T; + } + return (this.event.content || {}) as T; } /** @@ -497,7 +503,7 @@ export class MatrixEvent extends EventEmitter { } public shouldAttemptDecryption() { - return this.isEncrypted() && !this.isBeingDecrypted() && this.getClearContent() === null; + return this.isEncrypted() && !this.isBeingDecrypted() && !this.clearEvent; } /** @@ -529,10 +535,7 @@ export class MatrixEvent extends EventEmitter { throw new Error("Attempt to decrypt event which isn't encrypted"); } - if ( - this.clearEvent && this.clearEvent.content && - this.clearEvent.content.msgtype !== "m.bad.encrypted" - ) { + if (this.clearEvent && !this.isDecryptionFailure()) { // we may want to just ignore this? let's start with rejecting it. throw new Error( "Attempt to decrypt event which has already been decrypted", @@ -740,8 +743,7 @@ export class MatrixEvent extends EventEmitter { * @returns {Object} The cleartext (decrypted) content for the event */ public getClearContent(): IContent | null { - const ev = this.clearEvent; - return ev && ev.content ? ev.content : null; + return this.clearEvent ? this.clearEvent.content : null; } /** @@ -932,8 +934,8 @@ export class MatrixEvent extends EventEmitter { public getRedactionEvent(): object | null { if (!this.isRedacted()) return null; - if (this.clearEvent.unsigned) { - return this.clearEvent.unsigned.redacted_because; + if (this.clearEvent?.unsigned) { + return this.clearEvent?.unsigned.redacted_because; } else if (this.event.unsigned.redacted_because) { return this.event.unsigned.redacted_because; } else { diff --git a/src/models/room-member.ts b/src/models/room-member.ts index e7a98257b..e3d6b66ce 100644 --- a/src/models/room-member.ts +++ b/src/models/room-member.ts @@ -131,7 +131,11 @@ export class RoomMember extends EventEmitter { this.disambiguate, ); - this.rawDisplayName = event.getDirectionalContent().displayname || this.userId; + this.rawDisplayName = event.getDirectionalContent().displayname; + if (!this.rawDisplayName || !utils.removeHiddenChars(this.rawDisplayName)) { + this.rawDisplayName = this.userId; + } + if (oldMembership !== this.membership) { this.updateModifiedTime(); this.emit("RoomMember.membership", event, this, oldMembership); diff --git a/src/models/room-state.ts b/src/models/room-state.ts index 2b67ee79e..79ad48f8e 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -204,9 +204,9 @@ export class RoomState extends EventEmitter { * @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was * undefined, else a single event (or null if no match found). */ - public getStateEvents(eventType: string): MatrixEvent[]; - public getStateEvents(eventType: string, stateKey: string): MatrixEvent; - public getStateEvents(eventType: string, stateKey?: string) { + public getStateEvents(eventType: EventType | string): MatrixEvent[]; + public getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent; + public getStateEvents(eventType: EventType | string, stateKey?: string) { if (!this.events.has(eventType)) { // no match return stateKey === undefined ? [] : null; diff --git a/src/store/indexeddb-local-backend.ts b/src/store/indexeddb-local-backend.ts index 9a7e803bb..c5c25fc19 100644 --- a/src/store/indexeddb-local-backend.ts +++ b/src/store/indexeddb-local-backend.ts @@ -117,7 +117,7 @@ function reqAsCursorPromise(req: IDBRequest): Promise { dbName = "matrix-js-sdk:" + (dbName || "default"); return IndexedDBHelpers.exists(indexedDB, dbName); } diff --git a/src/store/indexeddb.ts b/src/store/indexeddb.ts index f41a898c6..51fa88d5f 100644 --- a/src/store/indexeddb.ts +++ b/src/store/indexeddb.ts @@ -47,7 +47,7 @@ interface IOpts extends IBaseOpts { } export class IndexedDBStore extends MemoryStore { - static exists(indexedDB: IDBFactory, dbName: string): boolean { + static exists(indexedDB: IDBFactory, dbName: string): Promise { return LocalIndexedDBStoreBackend.exists(indexedDB, dbName); } diff --git a/src/utils.ts b/src/utils.ts index e5734bd0c..0a0f259e4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -402,10 +402,11 @@ export function normalize(str: string): string { // various width spaces U+2000 - U+200D // LTR and RTL marks U+200E and U+200F // LTR/RTL and other directional formatting marks U+202A - U+202F +// Arabic Letter RTL mark U+061C // Combining characters U+0300 - U+036F // Zero width no-break space (BOM) U+FEFF // eslint-disable-next-line no-misleading-character-class -const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036f\uFEFF\s]/g; +const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036F\uFEFF\u061C\s]/g; export function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -693,3 +694,25 @@ const collator = new Intl.Collator(); export function compare(a: string, b: string): number { return collator.compare(a, b); } + +/** + * This function is similar to Object.assign() but it assigns recursively and + * allows you to ignore nullish values from the source + * + * @param {Object} target + * @param {Object} source + * @returns the target object + */ +export function recursivelyAssign(target: Object, source: Object, ignoreNullish = false): any { + for (const [sourceKey, sourceValue] of Object.entries(source)) { + if (target[sourceKey] instanceof Object && sourceValue) { + recursivelyAssign(target[sourceKey], sourceValue); + continue; + } + if ((sourceValue !== null && sourceValue !== undefined) || !ignoreNullish) { + target[sourceKey] = sourceValue; + continue; + } + } + return target; +} diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 05e64da2c..da529c08a 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -34,6 +34,9 @@ import { MCallOfferNegotiate, CallCapabilities, SDPStreamMetadataPurpose, + SDPStreamMetadata, + SDPStreamMetadataKey, + MCallSDPStreamMetadataChanged, } from './callEventTypes'; import { CallFeed } from './callFeed'; @@ -138,7 +141,7 @@ export enum CallErrorCode { UnknownDevices = 'unknown_devices', /** - * Error code usewd when we fail to send the invite + * Error code used when we fail to send the invite * for some reason other than there being unknown devices */ SendInvite = 'send_invite', @@ -149,7 +152,7 @@ export enum CallErrorCode { CreateAnswer = 'create_answer', /** - * Error code usewd when we fail to send the answer + * Error code used when we fail to send the answer * for some reason other than there being unknown devices */ SendAnswer = 'send_answer', @@ -235,7 +238,7 @@ export class CallError extends Error { code: string; constructor(code: CallErrorCode, msg: string, err: Error) { - // Stil ldon't think there's any way to have proper nested errors + // Still don't think there's any way to have proper nested errors super(msg + ": " + err); this.code = code; @@ -275,12 +278,10 @@ export class MatrixCall extends EventEmitter { private sentEndOfCandidates: boolean; private peerConn: RTCPeerConnection; private feeds: Array; - private screenSharingStream: MediaStream; - private localAVStream: MediaStream; + private usermediaSenders: Array; + private screensharingSenders: Array; private inviteOrAnswerSent: boolean; private waitForLocalAVStream: boolean; - // XXX: I don't know why this is called 'config'. - private config: MediaStreamConstraints; private successor: MatrixCall; private opponentMember: RoomMember; private opponentVersion: number; @@ -294,9 +295,6 @@ export class MatrixCall extends EventEmitter { // This flag represents whether we want the other party to be on hold private remoteOnHold; - private micMuted; - private vidMuted; - // the stats for the call at the point it ended. We can't get these after we // tear the call down, so we just grab a snapshot before we stop the call. // The typescript definitions have this type as 'any' :( @@ -313,6 +311,8 @@ export class MatrixCall extends EventEmitter { private remoteAssertedIdentity: AssertedIdentity; + private remoteSDPStreamMetadata: SDPStreamMetadata; + constructor(opts: CallOpts) { super(); this.roomId = opts.roomId; @@ -345,10 +345,11 @@ export class MatrixCall extends EventEmitter { this.makingOffer = false; this.remoteOnHold = false; - this.micMuted = false; - this.vidMuted = false; this.feeds = []; + + this.usermediaSenders = []; + this.screensharingSenders = []; } /** @@ -375,47 +376,6 @@ export class MatrixCall extends EventEmitter { await this.placeCallWithConstraints(constraints); } - /** - * Place a screen-sharing call to this room. This includes audio. - * This method is EXPERIMENTAL and subject to change without warning. It - * only works in Google Chrome and Firefox >= 44. - * @throws If you have not specified a listener for 'error' events. - */ - async placeScreenSharingCall(selectDesktopCapturerSource?: () => Promise) { - logger.debug("placeScreenSharingCall"); - this.checkForErrorListener(); - try { - const screenshareConstraints = await getScreenshareContraints(selectDesktopCapturerSource); - if (!screenshareConstraints) { - this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false); - return; - } - - if (window.electron?.getDesktopCapturerSources) { - // We are using Electron - logger.debug("Getting screen stream using getUserMedia()..."); - this.screenSharingStream = await navigator.mediaDevices.getUserMedia(screenshareConstraints); - } else { - // We are not using Electron - logger.debug("Getting screen stream using getDisplayMedia()..."); - this.screenSharingStream = await navigator.mediaDevices.getDisplayMedia(screenshareConstraints); - } - - logger.debug("Got screen stream, requesting audio stream..."); - const audioConstraints = getUserMediaContraints(ConstraintsType.Audio); - this.placeCallWithConstraints(audioConstraints); - } catch (err) { - this.emit(CallEvent.Error, - new CallError( - CallErrorCode.NoUserMedia, - "Failed to get screen-sharing stream: ", err, - ), - ); - this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false); - } - this.type = CallType.Video; - } - public getOpponentMember() { return this.opponentMember; } @@ -424,10 +384,34 @@ export class MatrixCall extends EventEmitter { return Boolean(this.opponentCaps && this.opponentCaps["m.call.transferee"]); } + public opponentSupportsDTMF(): boolean { + return Boolean(this.opponentCaps && this.opponentCaps["m.call.dtmf"]); + } + public getRemoteAssertedIdentity(): AssertedIdentity { return this.remoteAssertedIdentity; } + public get localUsermediaFeed(): CallFeed { + return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia); + } + + public get localScreensharingFeed(): CallFeed { + return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare); + } + + public get localUsermediaStream(): MediaStream { + return this.localUsermediaFeed?.stream; + } + + private get localScreensharingStream(): MediaStream { + return this.localScreensharingFeed?.stream; + } + + private getFeedByStreamId(streamId: string): CallFeed { + return this.getFeeds().find((feed) => feed.stream.id === streamId); + } + /** * Returns an array of all CallFeeds * @returns {Array} CallFeeds @@ -452,6 +436,23 @@ export class MatrixCall extends EventEmitter { return this.feeds.filter((feed) => !feed.isLocal()); } + /** + * Generates and returns localSDPStreamMetadata + * @returns {SDPStreamMetadata} localSDPStreamMetadata + */ + private getLocalSDPStreamMetadata(): SDPStreamMetadata { + const metadata: SDPStreamMetadata = {}; + for (const localFeed of this.getLocalFeeds()) { + metadata[localFeed.stream.id] = { + purpose: localFeed.purpose, + audio_muted: localFeed.isAudioMuted(), + video_muted: localFeed.isVideoMuted(), + }; + } + logger.debug("Got local SDPStreamMetadata", metadata); + return metadata; + } + /** * Returns true if there are no incoming feeds, * otherwise returns false @@ -461,16 +462,103 @@ export class MatrixCall extends EventEmitter { return !this.feeds.some((feed) => !feed.isLocal()); } - private pushNewFeed(stream: MediaStream, userId: string, purpose: SDPStreamMetadataPurpose) { + private pushRemoteFeed(stream: MediaStream) { + // Fallback to old behavior if the other side doesn't support SDPStreamMetadata + if (!this.opponentSupportsSDPStreamMetadata()) { + this.pushRemoteFeedWithoutMetadata(stream); + return; + } + + const userId = this.getOpponentMember().userId; + const purpose = this.remoteSDPStreamMetadata[stream.id].purpose; + const audioMuted = this.remoteSDPStreamMetadata[stream.id].audio_muted; + const videoMuted = this.remoteSDPStreamMetadata[stream.id].video_muted; + + if (!purpose) { + logger.warn(`Ignoring stream with id ${stream.id} because we didn't get any metadata about it`); + return; + } + + // Try to find a feed with the same purpose as the new stream, + // if we find it replace the old stream with the new one + const existingFeed = this.getRemoteFeeds().find((feed) => feed.purpose === purpose); + if (existingFeed) { + existingFeed.setNewStream(stream); + } else { + this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, audioMuted, videoMuted)); + this.emit(CallEvent.FeedsChanged, this.feeds); + } + + logger.info(`Pushed remote stream (id="${stream.id}", active="${stream.active}", purpose=${purpose})`); + } + + /** + * This method is used ONLY if the other client doesn't support sending SDPStreamMetadata + */ + private pushRemoteFeedWithoutMetadata(stream: MediaStream) { + const userId = this.getOpponentMember().userId; + // We can guess the purpose here since the other client can only send one stream + const purpose = SDPStreamMetadataPurpose.Usermedia; + const oldRemoteStream = this.feeds.find((feed) => !feed.isLocal())?.stream; + + // Note that we check by ID and always set the remote stream: Chrome appears + // to make new stream objects when transceiver directionality is changed and the 'active' + // status of streams change - Dave + // If we already have a stream, check this stream has the same id + if (oldRemoteStream && stream.id !== oldRemoteStream.id) { + logger.warn(`Ignoring new stream ID ${stream.id}: we already have stream ID ${oldRemoteStream.id}`); + return; + } + // Try to find a feed with the same stream id as the new stream, // if we find it replace the old stream with the new one - const feed = this.feeds.find((feed) => feed.stream.id === stream.id); + const feed = this.getFeedByStreamId(stream.id); if (feed) { feed.setNewStream(stream); } else { - this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId)); + this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, false, false)); this.emit(CallEvent.FeedsChanged, this.feeds); } + + logger.info(`Pushed remote stream (id="${stream.id}", active="${stream.active}")`); + } + + private pushLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true) { + const userId = this.client.getUserId(); + + // We try to replace an existing feed if there already is one with the same purpose + const existingFeed = this.getLocalFeeds().find((feed) => feed.purpose === purpose); + if (existingFeed) { + existingFeed.setNewStream(stream); + } else { + this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, false, false)); + this.emit(CallEvent.FeedsChanged, this.feeds); + } + + // why do we enable audio (and only audio) tracks here? -- matthew + setTracksEnabled(stream.getAudioTracks(), true); + + if (addToPeerConnection) { + const senderArray = purpose === SDPStreamMetadataPurpose.Usermedia ? + this.usermediaSenders : this.screensharingSenders; + // Empty the array + senderArray.splice(0, senderArray.length); + + this.emit(CallEvent.FeedsChanged, this.feeds); + for (const track of stream.getTracks()) { + logger.info( + `Adding track (` + + `id="${track.id}", ` + + `kind="${track.kind}", ` + + `streamId="${stream.id}", ` + + `streamPurpose="${purpose}"` + + `) to peer connection`, + ); + senderArray.push(this.peerConn.addTrack(track, stream)); + } + } + + logger.info(`Pushed local stream (id="${stream.id}", active="${stream.active}", purpose="${purpose}")`); } private deleteAllFeeds() { @@ -478,6 +566,19 @@ export class MatrixCall extends EventEmitter { this.emit(CallEvent.FeedsChanged, this.feeds); } + private deleteFeedByStream(stream: MediaStream) { + logger.debug(`Removing feed with stream id ${stream.id}`); + + const feed = this.getFeedByStreamId(stream.id); + if (!feed) { + logger.warn(`Didn't find the feed with stream id ${stream.id} to delete`); + return; + } + + this.feeds.splice(this.feeds.indexOf(feed), 1); + this.emit(CallEvent.FeedsChanged, this.feeds); + } + // The typescript definitions have this type as 'any' :( public async getCurrentCallStats(): Promise { if (this.callHasEnded()) { @@ -516,6 +617,13 @@ export class MatrixCall extends EventEmitter { logger.warn("Failed to get TURN credentials! Proceeding with call anyway..."); } + const sdpStreamMetadata = invite[SDPStreamMetadataKey]; + if (sdpStreamMetadata) { + this.updateRemoteSDPStreamMetadata(sdpStreamMetadata); + } else { + logger.debug("Did not get any SDPStreamMetadata! Can not send/receive multiple streams"); + } + this.peerConn = this.createPeerConnection(); // we must set the party ID before await-ing on anything: the call event // handler will start giving us more call events (eg. candidates) so if @@ -533,7 +641,7 @@ export class MatrixCall extends EventEmitter { const remoteStream = this.feeds.find((feed) => !feed.isLocal())?.stream; // According to previous comments in this file, firefox at some point did not - // add streams until media started ariving on them. Testing latest firefox + // add streams until media started arriving on them. Testing latest firefox // (81 at time of writing), this is no longer a problem, so let's do it the correct way. if (!remoteStream || remoteStream.getTracks().length === 0) { logger.error("No remote stream or no tracks after setting remote description!"); @@ -582,7 +690,7 @@ export class MatrixCall extends EventEmitter { logger.debug(`Answering call ${this.callId} of type ${this.type}`); - if (!this.localAVStream && !this.waitForLocalAVStream) { + if (!this.localUsermediaStream && !this.waitForLocalAVStream) { const constraints = getUserMediaContraints( this.type == CallType.Video ? ConstraintsType.Video: @@ -600,8 +708,8 @@ export class MatrixCall extends EventEmitter { this.getUserMediaFailed(e); return; } - } else if (this.localAVStream) { - this.gotUserMediaForAnswer(this.localAVStream); + } else if (this.localUsermediaStream) { + this.gotUserMediaForAnswer(this.localUsermediaStream); } else if (this.waitForLocalAVStream) { this.setState(CallState.WaitLocalMedia); } @@ -619,12 +727,10 @@ export class MatrixCall extends EventEmitter { newCall.waitForLocalAVStream = true; } else if (this.state === CallState.CreateOffer) { logger.debug("Handing local stream to new call"); - newCall.gotUserMediaForAnswer(this.localAVStream); - delete(this.localAVStream); + newCall.gotUserMediaForAnswer(this.localUsermediaStream); } else if (this.state === CallState.InviteSent) { logger.debug("Handing local stream to new call"); - newCall.gotUserMediaForAnswer(this.localAVStream); - delete(this.localAVStream); + newCall.gotUserMediaForAnswer(this.localUsermediaStream); } this.successor = newCall; this.emit(CallEvent.Replaced, newCall); @@ -644,9 +750,10 @@ export class MatrixCall extends EventEmitter { // We don't want to send hangup here if we didn't even get to sending an invite if (this.state === CallState.WaitLocalMedia) return; const content = {}; - // Continue to send no reason for user hangups temporarily, until - // clients understand the user_hangup reason (voip v1) - if (reason !== CallErrorCode.UserHangup) content['reason'] = reason; + // Don't send UserHangup reason to older clients + if ((this.opponentVersion && this.opponentVersion >= 1) || reason !== CallErrorCode.UserHangup) { + content["reason"] = reason; + } this.sendVoipEvent(EventType.CallHangup, content); } @@ -673,12 +780,129 @@ export class MatrixCall extends EventEmitter { this.sendVoipEvent(EventType.CallReject, {}); } + /** + * Returns true if this.remoteSDPStreamMetadata is defined, otherwise returns false + * @returns {boolean} can screenshare + */ + public opponentSupportsSDPStreamMetadata(): boolean { + return Boolean(this.remoteSDPStreamMetadata); + } + + /** + * If there is a screensharing stream returns true, otherwise returns false + * @returns {boolean} is screensharing + */ + public isScreensharing(): boolean { + return Boolean(this.localScreensharingStream); + } + + /** + * Starts/stops screensharing + * @param enabled the desired screensharing state + * @param selectDesktopCapturerSource callBack to select a screensharing stream on desktop + * @returns {boolean} new screensharing state + */ + public async setScreensharingEnabled( + enabled: boolean, + selectDesktopCapturerSource?: () => Promise, + ) { + // Skip if there is nothing to do + if (enabled && this.isScreensharing()) { + logger.warn(`There is already a screensharing stream - there is nothing to do!`); + return true; + } else if (!enabled && !this.isScreensharing()) { + logger.warn(`There already isn't a screensharing stream - there is nothing to do!`); + return false; + } + + // Fallback to replaceTrack() + if (!this.opponentSupportsSDPStreamMetadata()) { + return await this.setScreensharingEnabledWithoutMetadataSupport(enabled, selectDesktopCapturerSource); + } + + logger.debug(`Set screensharing enabled? ${enabled}`); + if (enabled) { + try { + const stream = await getScreensharingStream(selectDesktopCapturerSource); + if (!stream) return false; + this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare); + return true; + } catch (err) { + this.emit(CallEvent.Error, + new CallError(CallErrorCode.NoUserMedia, "Failed to get screen-sharing stream: ", err), + ); + return false; + } + } else { + for (const sender of this.screensharingSenders) { + this.peerConn.removeTrack(sender); + } + for (const track of this.localScreensharingStream.getTracks()) { + track.stop(); + } + this.deleteFeedByStream(this.localScreensharingStream); + return false; + } + } + + /** + * Starts/stops screensharing + * Should be used ONLY if the opponent doesn't support SDPStreamMetadata + * @param enabled the desired screensharing state + * @param selectDesktopCapturerSource callBack to select a screensharing stream on desktop + * @returns {boolean} new screensharing state + */ + private async setScreensharingEnabledWithoutMetadataSupport( + enabled: boolean, + selectDesktopCapturerSource?: () => Promise, + ) { + logger.debug(`Set screensharing enabled? ${enabled} using replaceTrack()`); + if (enabled) { + try { + const stream = await getScreensharingStream(selectDesktopCapturerSource); + if (!stream) return false; + + const track = stream.getTracks().find((track) => { + return track.kind === "video"; + }); + const sender = this.usermediaSenders.find((sender) => { + return sender.track?.kind === "video"; + }); + sender.replaceTrack(track); + + this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare, false); + + return true; + } catch (err) { + this.emit(CallEvent.Error, + new CallError(CallErrorCode.NoUserMedia, "Failed to get screen-sharing stream: ", err), + ); + return false; + } + } else { + const track = this.localUsermediaStream.getTracks().find((track) => { + return track.kind === "video"; + }); + const sender = this.usermediaSenders.find((sender) => { + return sender.track?.kind === "video"; + }); + sender.replaceTrack(track); + + for (const track of this.localScreensharingStream.getTracks()) { + track.stop(); + } + this.deleteFeedByStream(this.localScreensharingStream); + + return false; + } + } + /** * Set whether our outbound video should be muted or not. * @param {boolean} muted True to mute the outbound video. */ setLocalVideoMuted(muted: boolean) { - this.vidMuted = muted; + this.localUsermediaFeed?.setVideoMuted(muted); this.updateMuteStatus(); } @@ -692,7 +916,7 @@ export class MatrixCall extends EventEmitter { * (including if the call is not set up yet). */ isLocalVideoMuted(): boolean { - return this.vidMuted; + return this.localUsermediaFeed?.isVideoMuted(); } /** @@ -700,7 +924,7 @@ export class MatrixCall extends EventEmitter { * @param {boolean} muted True to mute the mic. */ setMicrophoneMuted(muted: boolean) { - this.micMuted = muted; + this.localUsermediaFeed?.setAudioMuted(muted); this.updateMuteStatus(); } @@ -714,7 +938,7 @@ export class MatrixCall extends EventEmitter { * is not set up yet). */ isMicrophoneMuted(): boolean { - return this.micMuted; + return this.localUsermediaFeed?.isAudioMuted(); } /** @@ -729,11 +953,11 @@ export class MatrixCall extends EventEmitter { if (this.isRemoteOnHold() === onHold) return; this.remoteOnHold = onHold; - for (const tranceiver of this.peerConn.getTransceivers()) { + for (const transceiver of this.peerConn.getTransceivers()) { // We don't send hold music or anything so we're not actually // sending anything, but sendrecv is fairly standard for hold and // it makes it a lot easier to figure out who's put who on hold. - tranceiver.direction = onHold ? 'sendonly' : 'sendrecv'; + transceiver.direction = onHold ? 'sendonly' : 'sendrecv'; } this.updateMuteStatus(); @@ -752,8 +976,8 @@ export class MatrixCall extends EventEmitter { // We consider a call to be on hold only if *all* the tracks are on hold // (is this the right thing to do?) - for (const tranceiver of this.peerConn.getTransceivers()) { - const trackOnHold = ['inactive', 'recvonly'].includes(tranceiver.currentDirection); + for (const transceiver of this.peerConn.getTransceivers()) { + const trackOnHold = ['inactive', 'recvonly'].includes(transceiver.currentDirection); if (!trackOnHold) callOnHold = false; } @@ -777,15 +1001,15 @@ export class MatrixCall extends EventEmitter { } private updateMuteStatus() { - if (!this.localAVStream) { - return; - } + this.sendVoipEvent(EventType.CallSDPStreamMetadataChangedPrefix, { + [SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(), + }); - const micShouldBeMuted = this.micMuted || this.remoteOnHold; - setTracksEnabled(this.localAVStream.getAudioTracks(), !micShouldBeMuted); + const micShouldBeMuted = this.localUsermediaFeed?.isAudioMuted() || this.remoteOnHold; + const vidShouldBeMuted = this.localUsermediaFeed?.isVideoMuted() || this.remoteOnHold; - const vidShouldBeMuted = this.vidMuted || this.remoteOnHold; - setTracksEnabled(this.localAVStream.getVideoTracks(), !vidShouldBeMuted); + setTracksEnabled(this.localUsermediaStream.getAudioTracks(), !micShouldBeMuted); + setTracksEnabled(this.localUsermediaStream.getVideoTracks(), !vidShouldBeMuted); } /** @@ -801,34 +1025,12 @@ export class MatrixCall extends EventEmitter { this.stopAllMedia(); return; } - this.localAVStream = stream; - logger.info("Got local AV stream with id " + this.localAVStream.id); + this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); this.setState(CallState.CreateOffer); + logger.info("Got local AV stream with id " + this.localUsermediaStream.id); logger.debug("gotUserMediaForInvite -> " + this.type); - - if (this.screenSharingStream) { - logger.debug( - "Setting screen sharing stream to the local video element", - ); - this.pushNewFeed(this.screenSharingStream, this.client.getUserId(), SDPStreamMetadataPurpose.Screenshare); - } else { - this.pushNewFeed(stream, this.client.getUserId(), SDPStreamMetadataPurpose.Usermedia); - } - - // why do we enable audio (and only audio) tracks here? -- matthew - setTracksEnabled(stream.getAudioTracks(), true); - - for (const audioTrack of stream.getAudioTracks()) { - logger.info("Adding audio track with id " + audioTrack.id); - this.peerConn.addTrack(audioTrack, stream); - } - for (const videoTrack of (this.screenSharingStream || stream).getVideoTracks()) { - logger.info("Adding video track with id " + videoTrack.id); - this.peerConn.addTrack(videoTrack, stream); - } - // Now we wait for the negotiationneeded event }; @@ -840,15 +1042,15 @@ export class MatrixCall extends EventEmitter { // required to still be sent for backwards compat type: this.peerConn.localDescription.type, }, + [SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(), } as MCallAnswer; - if (this.client.supportsCallTransfer) { - answerContent.capabilities = { - 'm.call.transferee': true, - }; - } + answerContent.capabilities = { + 'm.call.transferee': this.client.supportsCallTransfer, + 'm.call.dtmf': false, + }; - // We have just taken the local description from the peerconnection which will + // We have just taken the local description from the peerConn which will // contain all the local candidates added so far, so we can discard any candidates // we had queued up because they'll be in the answer. logger.info(`Discarding ${this.candidateSendQueue.length} candidates that will be sent in answer`); @@ -884,19 +1086,15 @@ export class MatrixCall extends EventEmitter { return; } - this.pushNewFeed(stream, this.client.getUserId(), SDPStreamMetadataPurpose.Usermedia); + this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); - this.localAVStream = stream; - logger.info("Got local AV stream with id " + this.localAVStream.id); - setTracksEnabled(stream.getAudioTracks(), true); - for (const track of stream.getTracks()) { - this.peerConn.addTrack(track, stream); - } + logger.info("Got local AV stream with id " + this.localUsermediaStream.id); this.setState(CallState.CreateAnswer); let myAnswer; try { + this.getRidOfRTXCodecs(); myAnswer = await this.peerConn.createAnswer(); } catch (err) { logger.debug("Failed to create answer: ", err); @@ -968,8 +1166,8 @@ export class MatrixCall extends EventEmitter { return; } - const cands = ev.getContent().candidates; - if (!cands) { + const candidates = ev.getContent().candidates; + if (!candidates) { logger.info("Ignoring candidates event with no candidates!"); return; } @@ -978,10 +1176,10 @@ export class MatrixCall extends EventEmitter { if (this.opponentPartyId === undefined) { // we haven't picked an opponent yet so save the candidates - logger.info(`Bufferring ${cands.length} candidates until we pick an opponent`); - const bufferedCands = this.remoteCandidateBuffer.get(fromPartyId) || []; - bufferedCands.push(...cands); - this.remoteCandidateBuffer.set(fromPartyId, bufferedCands); + logger.info(`Buffering ${candidates.length} candidates until we pick an opponent`); + const bufferedCandidates = this.remoteCandidateBuffer.get(fromPartyId) || []; + bufferedCandidates.push(...candidates); + this.remoteCandidateBuffer.set(fromPartyId, bufferedCandidates); return; } @@ -994,7 +1192,7 @@ export class MatrixCall extends EventEmitter { return; } - await this.addIceCandidates(cands); + await this.addIceCandidates(candidates); } /** @@ -1022,6 +1220,13 @@ export class MatrixCall extends EventEmitter { this.setState(CallState.Connecting); + const sdpStreamMetadata = event.getContent()[SDPStreamMetadataKey]; + if (sdpStreamMetadata) { + this.updateRemoteSDPStreamMetadata(sdpStreamMetadata); + } else { + logger.warn("Did not get any SDPStreamMetadata! Can not send/receive multiple streams"); + } + try { await this.peerConn.setRemoteDescription(event.getContent().answer); } catch (e) { @@ -1092,15 +1297,24 @@ export class MatrixCall extends EventEmitter { const prevLocalOnHold = this.isLocalOnHold(); + const sdpStreamMetadata = event.getContent()[SDPStreamMetadataKey]; + if (sdpStreamMetadata) { + this.updateRemoteSDPStreamMetadata(sdpStreamMetadata); + } else { + logger.warn("Received negotiation event without SDPStreamMetadata!"); + } + try { await this.peerConn.setRemoteDescription(description); if (description.type === 'offer') { + this.getRidOfRTXCodecs(); const localDescription = await this.peerConn.createAnswer(); await this.peerConn.setLocalDescription(localDescription); this.sendVoipEvent(EventType.CallNegotiate, { description: this.peerConn.localDescription, + [SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(), }); } } catch (err) { @@ -1115,6 +1329,22 @@ export class MatrixCall extends EventEmitter { } } + private updateRemoteSDPStreamMetadata(metadata: SDPStreamMetadata): void { + this.remoteSDPStreamMetadata = utils.recursivelyAssign(this.remoteSDPStreamMetadata || {}, metadata, true); + for (const feed of this.getRemoteFeeds()) { + const streamId = feed.stream.id; + feed.setAudioMuted(this.remoteSDPStreamMetadata[streamId]?.audio_muted); + feed.setVideoMuted(this.remoteSDPStreamMetadata[streamId]?.video_muted); + feed.purpose = this.remoteSDPStreamMetadata[streamId]?.purpose; + } + } + + public onSDPStreamMetadataChangedReceived(event: MatrixEvent): void { + const content = event.getContent(); + const metadata = content[SDPStreamMetadataKey]; + this.updateRemoteSDPStreamMetadata(metadata); + } + async onAssertedIdentityReceived(event: MatrixEvent) { if (!event.getContent().asserted_identity) return; @@ -1164,18 +1394,19 @@ export class MatrixCall extends EventEmitter { lifetime: CALL_TIMEOUT_MS, } as MCallOfferNegotiate; - // clunky because TypeScript can't folow the types through if we use an expression as the key + // clunky because TypeScript can't follow the types through if we use an expression as the key if (this.state === CallState.CreateOffer) { content.offer = this.peerConn.localDescription; } else { content.description = this.peerConn.localDescription; } - if (this.client.supportsCallTransfer) { - content.capabilities = { - 'm.call.transferee': true, - }; - } + content.capabilities = { + 'm.call.transferee': this.client.supportsCallTransfer, + 'm.call.dtmf': false, + }; + + content[SDPStreamMetadataKey] = this.getLocalSDPStreamMetadata(); // Get rid of any candidates waiting to be sent: they'll be included in the local // description we just got and will send in the offer. @@ -1281,34 +1512,54 @@ export class MatrixCall extends EventEmitter { return; } - const oldRemoteStream = this.feeds.find((feed) => !feed.isLocal())?.stream; - - // If we already have a stream, check this track is from the same one - // Note that we check by ID and always set the remote stream: Chrome appears - // to make new stream objects when tranciever directionality is changed and the 'active' - // status of streams change - Dave - if (oldRemoteStream && ev.streams[0].id !== oldRemoteStream.id) { - logger.warn( - `Ignoring new stream ID ${ev.streams[0].id}: we already have stream ID ${oldRemoteStream.id}`, - ); - return; - } - - if (!oldRemoteStream) { - logger.info("Got remote stream with id " + ev.streams[0].id); - } - - const newRemoteStream = ev.streams[0]; - - logger.debug(`Track id ${ev.track.id} of kind ${ev.track.kind} added`); - - this.pushNewFeed(newRemoteStream, this.getOpponentMember().userId, SDPStreamMetadataPurpose.Usermedia); - - logger.info("playing remote. stream active? " + newRemoteStream.active); + const stream = ev.streams[0]; + this.pushRemoteFeed(stream); + stream.addEventListener("removetrack", () => this.deleteFeedByStream(stream)); }; + /** + * This method removes all video/rtx codecs from screensharing video + * transceivers. This is necessary since they can cause problems. Without + * this the following steps should produce an error: + * Chromium calls Firefox + * Firefox answers + * Firefox starts screen-sharing + * Chromium starts screen-sharing + * Call crashes for Chromium with: + * [96685:23:0518/162603.933321:ERROR:webrtc_video_engine.cc(3296)] RTX codec (PT=97) mapped to PT=96 which is not in the codec list. + * [96685:23:0518/162603.933377:ERROR:webrtc_video_engine.cc(1171)] GetChangedRecvParameters called without any video codecs. + * [96685:23:0518/162603.933430:ERROR:sdp_offer_answer.cc(4302)] Failed to set local video description recv parameters for m-section with mid='2'. (INVALID_PARAMETER) + */ + private getRidOfRTXCodecs() { + // RTCRtpReceiver.getCapabilities and RTCRtpSender.getCapabilities don't seem to be supported on FF + if (!RTCRtpReceiver.getCapabilities || !RTCRtpSender.getCapabilities) return; + + const recvCodecs = RTCRtpReceiver.getCapabilities("video").codecs; + const sendCodecs = RTCRtpSender.getCapabilities("video").codecs; + const codecs = [...sendCodecs, ...recvCodecs]; + + for (const codec of codecs) { + if (codec.mimeType === "video/rtx") { + const rtxCodecIndex = codecs.indexOf(codec); + codecs.splice(rtxCodecIndex, 1); + } + } + + for (const trans of this.peerConn.getTransceivers()) { + if ( + this.screensharingSenders.includes(trans.sender) && + ( + trans.sender.track?.kind === "video" || + trans.receiver.track?.kind === "video" + ) + ) { + trans.setCodecPreferences(codecs); + } + } + } + onNegotiationNeeded = async () => { - logger.info("Negotation is needed!"); + logger.info("Negotiation is needed!"); if (this.state !== CallState.CreateOffer && this.opponentVersion === 0) { logger.info("Opponent does not support renegotiation: ignoring negotiationneeded event"); @@ -1317,6 +1568,7 @@ export class MatrixCall extends EventEmitter { this.makingOffer = true; try { + this.getRidOfRTXCodecs(); const myOffer = await this.peerConn.createOffer(); await this.gotLocalOffer(myOffer); } catch (e) { @@ -1395,12 +1647,12 @@ export class MatrixCall extends EventEmitter { // Don't send the ICE candidates yet if the call is in the ringing state: this // means we tried to pick (ie. started generating candidates) and then failed to // send the answer and went back to the ringing state. Queue up the candidates - // to send if we sucessfully send the answer. + // to send if we successfully send the answer. // Equally don't send if we haven't yet sent the answer because we can send the // first batch of candidates along with the answer if (this.state === CallState.Ringing || !this.inviteOrAnswerSent) return; - // MSC2746 reccomends these values (can be quite long when calling because the + // MSC2746 recommends these values (can be quite long when calling because the // callee will need a while to answer the call) const delay = this.direction === CallDirection.Inbound ? 500 : 2000; @@ -1416,7 +1668,7 @@ export class MatrixCall extends EventEmitter { */ async transfer(targetUserId: string) { // Fetch the target user's global profile info: their room avatar / displayname - // could be different in whatever room we shae with them. + // could be different in whatever room we share with them. const profileInfo = await this.client.getProfileInfo(targetUserId); const replacementId = genCallID(); @@ -1503,7 +1755,7 @@ export class MatrixCall extends EventEmitter { } private stopAllMedia() { - logger.debug(`stopAllMedia (stream=${this.localAVStream})`); + logger.debug(`stopAllMedia (stream=${this.localUsermediaStream})`); for (const feed of this.feeds) { for (const track of feed.stream.getTracks()) { @@ -1525,13 +1777,13 @@ export class MatrixCall extends EventEmitter { return; } - const cands = this.candidateSendQueue; + const candidates = this.candidateSendQueue; this.candidateSendQueue = []; ++this.candidateSendTries; const content = { - candidates: cands, + candidates: candidates, }; - logger.debug("Attempting to send " + cands.length + " candidates"); + logger.debug("Attempting to send " + candidates.length + " candidates"); try { await this.sendVoipEvent(EventType.CallCandidates, content); } catch (error) { @@ -1540,7 +1792,7 @@ export class MatrixCall extends EventEmitter { if (error.event) this.client.cancelPendingEvent(error.event); // put all the candidates we failed to send back in the queue - this.candidateSendQueue.push(...cands); + this.candidateSendQueue.push(...candidates); if (this.candidateSendTries > 5) { logger.debug( @@ -1572,7 +1824,6 @@ export class MatrixCall extends EventEmitter { this.client.callEventHandler.calls.set(this.callId, this); this.setState(CallState.WaitLocalMedia); this.direction = CallDirection.Outbound; - this.config = constraints; // make sure we have valid turn creds. Unless something's gone wrong, it should // poll and keep the credentials valid so this should be instant. @@ -1645,26 +1896,28 @@ export class MatrixCall extends EventEmitter { } private async addBufferedIceCandidates() { - const bufferedCands = this.remoteCandidateBuffer.get(this.opponentPartyId); - if (bufferedCands) { - logger.info(`Adding ${bufferedCands.length} buffered candidates for opponent ${this.opponentPartyId}`); - await this.addIceCandidates(bufferedCands); + const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId); + if (bufferedCandidates) { + logger.info(`Adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`); + await this.addIceCandidates(bufferedCandidates); } this.remoteCandidateBuffer = null; } - private async addIceCandidates(cands: RTCIceCandidate[]) { - for (const cand of cands) { + private async addIceCandidates(candidates: RTCIceCandidate[]) { + for (const candidate of candidates) { if ( - (cand.sdpMid === null || cand.sdpMid === undefined) && - (cand.sdpMLineIndex === null || cand.sdpMLineIndex === undefined) + (candidate.sdpMid === null || candidate.sdpMid === undefined) && + (candidate.sdpMLineIndex === null || candidate.sdpMLineIndex === undefined) ) { logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex"); continue; } - logger.debug("Call " + this.callId + " got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate); + logger.debug( + "Call " + this.callId + " got remote ICE " + candidate.sdpMid + " candidate: " + candidate.candidate, + ); try { - await this.peerConn.addIceCandidate(cand); + await this.peerConn.addIceCandidate(candidate); } catch (err) { if (!this.ignoreOffer) { logger.info("Failed to add remote ICE candidate", err); @@ -1674,6 +1927,23 @@ export class MatrixCall extends EventEmitter { } } +async function getScreensharingStream( + selectDesktopCapturerSource?: () => Promise, +): Promise { + const screenshareConstraints = await getScreenshareContraints(selectDesktopCapturerSource); + if (!screenshareConstraints) return null; + + if (window.electron?.getDesktopCapturerSources) { + // We are using Electron + logger.debug("Getting screen stream using getUserMedia()..."); + return await navigator.mediaDevices.getUserMedia(screenshareConstraints); + } else { + // We are not using Electron + logger.debug("Getting screen stream using getDisplayMedia()..."); + return await navigator.mediaDevices.getDisplayMedia(screenshareConstraints); + } +} + function setTracksEnabled(tracks: Array, enabled: boolean) { for (let i = 0; i < tracks.length; i++) { tracks[i].enabled = enabled; diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 263ddbf9b..3e080c0eb 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -130,21 +130,19 @@ export class CallEventHandler { private handleCallEvent(event: MatrixEvent) { const content = event.getContent(); + const type = event.getType() as EventType; + const weSentTheEvent = event.getSender() === this.client.credentials.userId; let call = content.call_id ? this.calls.get(content.call_id) : undefined; - //console.info("RECV %s content=%s", event.getType(), JSON.stringify(content)); + //console.info("RECV %s content=%s", type, JSON.stringify(content)); - if (event.getType() === EventType.CallInvite) { - if (event.getSender() === this.client.credentials.userId) { - return; // ignore invites you send - } + if (type === EventType.CallInvite) { + // ignore invites you send + if (weSentTheEvent) return; + // expired call + if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) return; + // stale/old invite event + if (call && call.state === CallState.Ended) return; - if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) { - return; // expired call - } - - if (call && call.state === CallState.Ended) { - return; // stale/old invite event - } if (call) { logger.log( `WARN: Already have a MatrixCall with id ${content.call_id} but got an ` + @@ -154,9 +152,11 @@ export class CallEventHandler { const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now(); logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); - call = createNewMatrixCall(this.client, event.getRoomId(), { - forceTURN: this.client.forceTURN, - }); + call = createNewMatrixCall( + this.client, + event.getRoomId(), + { forceTURN: this.client.forceTURN }, + ); if (!call) { logger.log( "Incoming call ID " + content.call_id + " but this client " + @@ -220,21 +220,9 @@ export class CallEventHandler { } else { this.client.emit("Call.incoming", call); } - } else if (event.getType() === EventType.CallAnswer) { - if (!call) { - return; - } - if (event.getSender() === this.client.credentials.userId) { - if (call.state === CallState.Ringing) { - call.onAnsweredElsewhere(content); - } - } else { - call.onAnswerReceived(event); - } - } else if (event.getType() === EventType.CallCandidates) { - if (event.getSender() === this.client.credentials.userId) { - return; - } + } else if (type === EventType.CallCandidates) { + if (weSentTheEvent) return; + if (!call) { // store the candidates; we may get a call eventually. if (!this.candidateEventsByCall.has(content.call_id)) { @@ -244,7 +232,7 @@ export class CallEventHandler { } else { call.onRemoteIceCandidatesReceived(event); } - } else if ([EventType.CallHangup, EventType.CallReject].includes(event.getType() as EventType)) { + } else if ([EventType.CallHangup, EventType.CallReject].includes(type)) { // Note that we also observe our own hangups here so we can see // if we've already rejected a call that would otherwise be valid if (!call) { @@ -259,7 +247,7 @@ export class CallEventHandler { } } else { if (call.state !== CallState.Ended) { - if (event.getType() === EventType.CallHangup) { + if (type === EventType.CallHangup) { call.onHangupReceived(content); } else { call.onRejectReceived(content); @@ -267,36 +255,40 @@ export class CallEventHandler { this.calls.delete(content.call_id); } } - } else if (event.getType() === EventType.CallSelectAnswer) { - if (!call) return; + } - if (event.getContent().party_id === call.ourPartyId) { - // Ignore remote echo - return; - } + // The following events need a call + if (!call) return; + // Ignore remote echo + if (event.getContent().party_id === call.ourPartyId) return; - call.onSelectAnswerReceived(event); - } else if (event.getType() === EventType.CallNegotiate) { - if (!call) return; + switch (type) { + case EventType.CallAnswer: + if (weSentTheEvent) { + if (call.state === CallState.Ringing) { + call.onAnsweredElsewhere(content); + } + } else { + call.onAnswerReceived(event); + } + break; + case EventType.CallSelectAnswer: + call.onSelectAnswerReceived(event); + break; - if (event.getContent().party_id === call.ourPartyId) { - // Ignore remote echo - return; - } + case EventType.CallNegotiate: + call.onNegotiateReceived(event); + break; - call.onNegotiateReceived(event); - } else if ( - event.getType() === EventType.CallAssertedIdentity || - event.getType() === EventType.CallAssertedIdentityPrefix - ) { - if (!call) return; + case EventType.CallAssertedIdentity: + case EventType.CallAssertedIdentityPrefix: + call.onAssertedIdentityReceived(event); + break; - if (event.getContent().party_id === call.ourPartyId) { - // Ignore remote echo (not that we send asserted identity, but still...) - return; - } - - call.onAssertedIdentityReceived(event); + case EventType.CallSDPStreamMetadataChanged: + case EventType.CallSDPStreamMetadataChangedPrefix: + call.onSDPStreamMetadataChangedReceived(event); + break; } } } diff --git a/src/webrtc/callEventTypes.ts b/src/webrtc/callEventTypes.ts index dce146485..009df5ed8 100644 --- a/src/webrtc/callEventTypes.ts +++ b/src/webrtc/callEventTypes.ts @@ -1,11 +1,24 @@ -// allow camelcase as these are events type that go onto the wire +// allow non-camelcase as these are events type that go onto the wire /* eslint-disable camelcase */ +// TODO: Change to "sdp_stream_metadata" when MSC3077 is merged +export const SDPStreamMetadataKey = "org.matrix.msc3077.sdp_stream_metadata"; + export enum SDPStreamMetadataPurpose { Usermedia = "m.usermedia", Screenshare = "m.screenshare", } +export interface SDPStreamMetadataObject { + purpose: SDPStreamMetadataPurpose; + audio_muted: boolean; + video_muted: boolean; +} + +export interface SDPStreamMetadata { + [key: string]: SDPStreamMetadataObject; +} + interface CallOfferAnswer { type: string; sdp: string; @@ -13,11 +26,19 @@ interface CallOfferAnswer { export interface CallCapabilities { 'm.call.transferee': boolean; + 'm.call.dtmf': boolean; +} + +export interface CallReplacesTarget { + id: string; + display_name: string; + avatar_url: string; } export interface MCallAnswer { answer: CallOfferAnswer; capabilities: CallCapabilities; + [SDPStreamMetadataKey]: SDPStreamMetadata; } export interface MCallOfferNegotiate { @@ -25,17 +46,16 @@ export interface MCallOfferNegotiate { description: CallOfferAnswer; lifetime: number; capabilities: CallCapabilities; + [SDPStreamMetadataKey]: SDPStreamMetadata; } -export interface MCallReplacesTarget { - id: string; - display_name: string; - avatar_url: string; +export interface MCallSDPStreamMetadataChanged { + [SDPStreamMetadataKey]: SDPStreamMetadata; } export interface MCallReplacesEvent { replacement_id: string; - target_user: MCallReplacesTarget; + target_user: CallReplacesTarget; create_call: string; await_call: string; target_room: string; diff --git a/src/webrtc/callFeed.ts b/src/webrtc/callFeed.ts index 69d6170ab..b82c68535 100644 --- a/src/webrtc/callFeed.ts +++ b/src/webrtc/callFeed.ts @@ -21,6 +21,7 @@ import { RoomMember } from "../models/room-member"; export enum CallFeedEvent { NewStream = "new_stream", + MuteStateChanged = "mute_state_changed" } export class CallFeed extends EventEmitter { @@ -30,6 +31,8 @@ export class CallFeed extends EventEmitter { public purpose: SDPStreamMetadataPurpose, private client: MatrixClient, private roomId: string, + private audioMuted: boolean, + private videoMuted: boolean, ) { super(); } @@ -51,15 +54,13 @@ export class CallFeed extends EventEmitter { return this.userId === this.client.getUserId(); } - // TODO: The two following methods should be later replaced - // by something that will also check if the remote is muted /** * Returns true if audio is muted or if there are no audio * tracks, otherwise returns false * @returns {boolean} is audio muted? */ public isAudioMuted(): boolean { - return this.stream.getAudioTracks().length === 0; + return this.stream.getAudioTracks().length === 0 || this.audioMuted; } /** @@ -69,7 +70,7 @@ export class CallFeed extends EventEmitter { */ public isVideoMuted(): boolean { // We assume only one video track - return this.stream.getVideoTracks().length === 0; + return this.stream.getVideoTracks().length === 0 || this.videoMuted; } /** @@ -81,4 +82,14 @@ export class CallFeed extends EventEmitter { this.stream = newStream; this.emit(CallFeedEvent.NewStream, this.stream); } + + public setAudioMuted(muted: boolean): void { + this.audioMuted = muted; + this.emit(CallFeedEvent.MuteStateChanged, this.audioMuted, this.videoMuted); + } + + public setVideoMuted(muted: boolean): void { + this.videoMuted = muted; + this.emit(CallFeedEvent.MuteStateChanged, this.audioMuted, this.videoMuted); + } } diff --git a/tsconfig-build.json b/tsconfig-build.json new file mode 100644 index 000000000..50f5536d9 --- /dev/null +++ b/tsconfig-build.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "./spec/**/*.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 548bbe7fb..896c4c3b3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,12 +9,10 @@ "noImplicitAny": false, "sourceMap": true, "outDir": "./lib", - "declaration": true, - "types": [ - "node" - ] + "declaration": true }, "include": [ - "./src/**/*.ts" + "./src/**/*.ts", + "./spec/**/*.ts", ] } diff --git a/yarn.lock b/yarn.lock index 563a8e3ba..2a431ea8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1228,6 +1228,107 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@octokit/auth-token@^2.4.4": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3" + integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA== + dependencies: + "@octokit/types" "^6.0.3" + +"@octokit/core@^3.5.0": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" + integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== + dependencies: + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.6.0" + "@octokit/request-error" "^2.0.5" + "@octokit/types" "^6.0.3" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.12" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658" + integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA== + dependencies: + "@octokit/types" "^6.0.3" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^4.5.8": + version "4.6.4" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.6.4.tgz#0c3f5bed440822182e972317122acb65d311a5ed" + integrity sha512-SWTdXsVheRmlotWNjKzPOb6Js6tjSqA2a8z9+glDJng0Aqjzti8MEWOtuT8ZSu6wHnci7LZNuarE87+WJBG4vg== + dependencies: + "@octokit/request" "^5.6.0" + "@octokit/types" "^6.0.3" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.3.0.tgz#160347858d727527901c6aae7f7d5c2414cc1f2e" + integrity sha512-oz60hhL+mDsiOWhEwrj5aWXTOMVtQgcvP+sRzX4C3cH7WOK9QSAoEtjWh0HdOf6V3qpdgAmUMxnQPluzDWR7Fw== + +"@octokit/plugin-paginate-rest@^2.6.2": + version "2.15.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.0.tgz#9c956c3710b2bd786eb3814eaf5a2b17392c150d" + integrity sha512-/vjcb0w6ggVRtsb8OJBcRR9oEm+fpdo8RJk45khaWw/W0c8rlB2TLCLyZt/knmC17NkX7T9XdyQeEY7OHLSV1g== + dependencies: + "@octokit/types" "^6.23.0" + +"@octokit/plugin-request-log@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" + integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== + +"@octokit/plugin-rest-endpoint-methods@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.6.0.tgz#c28833b88d0f07bf94093405d02d43d73c7de99b" + integrity sha512-2G7lIPwjG9XnTlNhe/TRnpI8yS9K2l68W4RP/ki3wqw2+sVeTK8hItPxkqEI30VeH0UwnzpuksMU/yHxiVVctw== + dependencies: + "@octokit/types" "^6.23.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" + integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== + dependencies: + "@octokit/types" "^6.0.3" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.0.tgz#6084861b6e4fa21dc40c8e2a739ec5eff597e672" + integrity sha512-4cPp/N+NqmaGQwbh3vUsYqokQIzt7VjsgTYVXiwpUP2pxd5YiZB2XuTedbb0SPtv9XS7nzAKjAuQxmY8/aZkiA== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.1.0" + "@octokit/types" "^6.16.1" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" + universal-user-agent "^6.0.0" + +"@octokit/rest@^18.6.7": + version "18.8.0" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.8.0.tgz#ba24f7ba554f015a7ae2b7cc2aecef5386ddfea5" + integrity sha512-lsuNRhgzGnLMn/NmQTNCit/6jplFWiTUlPXhqN0zCMLwf2/9pseHzsnTW+Cjlp4bLMEJJNPa5JOzSLbSCOahKw== + dependencies: + "@octokit/core" "^3.5.0" + "@octokit/plugin-paginate-rest" "^2.6.2" + "@octokit/plugin-request-log" "^1.0.2" + "@octokit/plugin-rest-endpoint-methods" "5.6.0" + +"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.23.0": + version "6.23.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.23.0.tgz#b39f242b20036e89fa8f34f7962b4e9b7ff8f65b" + integrity sha512-eG3clC31GSS7K3oBK6C6o7wyXPrkP+mu++eus8CSZdpRytJ5PNszYxudOQ0spWZQ3S9KAtoTG6v1WK5prJcJrA== + dependencies: + "@octokit/openapi-types" "^9.3.0" + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -1572,6 +1673,17 @@ align-text@^0.1.1, align-text@^0.1.3: longest "^1.0.1" repeat-string "^1.5.2" +"allchange@github:matrix-org/allchange": + version "0.0.1" + resolved "https://codeload.github.com/matrix-org/allchange/tar.gz/56b37b06339a3ac3fe771f3ec3d0bff798df8dab" + dependencies: + "@octokit/rest" "^18.6.7" + cli-color "^2.0.0" + js-yaml "^4.1.0" + loglevel "^1.7.1" + semver "^7.3.5" + yargs "^17.0.1" + another-json@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/another-json/-/another-json-0.2.0.tgz#b5f4019c973b6dd5c6506a2d93469cb6d32aeedc" @@ -1589,6 +1701,11 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" +ansi-regex@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" @@ -1636,6 +1753,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -1910,16 +2032,24 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -better-docs@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/better-docs/-/better-docs-2.3.2.tgz#0de059301c49669a4350409d8c235868cf8bcbf7" - integrity sha512-VlbXQgEftaynJSaPa853XB5WqTlPoQQr2TnxIkKi6OsyJJxF42Ke+9SIES/hqTe58aaBnuoDGrIzOso8RdNx6Q== +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== + +better-docs@^2.4.0-beta.9: + version "2.4.0-beta.9" + resolved "https://registry.yarnpkg.com/better-docs/-/better-docs-2.4.0-beta.9.tgz#9ffa25b90b9a0fe4eb97528faedf53c04c416cfe" + integrity sha512-ehR/4gxVE8+hIdiDN2sP/YOYxbOptUpuXcV0qMcN2snyn+gWAtFBqXmrRUcFJRj+e6HCdD3I2fK8Jy50+cvhVg== dependencies: brace "^0.11.1" + marked "^1.1.1" + prism-react-renderer "^1.1.1" react-ace "^6.5.0" react-docgen "^5.3.0" react-frame-component "^4.1.1" - typescript "^3.7.5" + react-live "^2.2.2" + react-simple-code-editor "^0.11.0" underscore "^1.9.1" vue-docgen-api "^3.22.0" vue2-ace-editor "^0.0.13" @@ -2159,6 +2289,18 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buble@0.19.6: + version "0.19.6" + resolved "https://registry.yarnpkg.com/buble/-/buble-0.19.6.tgz#915909b6bd5b11ee03b1c885ec914a8b974d34d3" + integrity sha512-9kViM6nJA1Q548Jrd06x0geh+BG2ru2+RMDkIHHgJY/8AcyCs34lTHwra9BX7YdPrZXd5aarkpr/SY8bmPgPdg== + dependencies: + chalk "^2.4.1" + magic-string "^0.25.1" + minimist "^1.2.0" + os-homedir "^1.0.1" + regexpu-core "^4.2.0" + vlq "^1.0.0" + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -2280,7 +2422,7 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2359,6 +2501,18 @@ clean-css@^4.1.11: dependencies: source-map "~0.6.0" +cli-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.0.tgz#11ecfb58a79278cf6035a60c54e338f9d837897c" + integrity sha512-a0VZ8LeraW0jTuCkuAGMNufareGHhyZU9z8OGsW0gXd1hZGi1SRuNRXdbGkraBBKnhyUhyebFWnRbp+dIn0f0A== + dependencies: + ansi-regex "^2.1.1" + d "^1.0.1" + es5-ext "^0.10.51" + es6-iterator "^2.0.3" + memoizee "^0.4.14" + timers-ext "^0.1.7" + cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -2479,6 +2633,16 @@ component-emitter@^1.2.1: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== +component-props@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/component-props/-/component-props-1.1.1.tgz#f9b7df9b9927b6e6d97c9bd272aa867670f34944" + integrity sha1-+bffm5kntubZfJvScqqGdnDzSUQ= + +component-xor@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/component-xor/-/component-xor-0.0.4.tgz#c55d83ccc1b94cd5089a4e93fa7891c7263e59aa" + integrity sha1-xV2DzMG5TNUImk6T+niRxyY+Wao= + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -2544,7 +2708,7 @@ core-js-compat@^3.14.0, core-js-compat@^3.15.0: browserslist "^4.16.6" semver "7.0.0" -core-js@^2.4.0, core-js@^2.5.3, core-js@^2.6.5: +core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.3, core-js@^2.6.5: version "2.6.12" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== @@ -2639,6 +2803,14 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + dash-ast@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dash-ast/-/dash-ast-1.0.0.tgz#12029ba5fb2f8aa6f0a861795b23c1b4b6c27d37" @@ -2743,6 +2915,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + deps-sort@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.1.tgz#9dfdc876d2bcec3386b6829ac52162cda9fa208d" @@ -2818,6 +2995,14 @@ doctypes@^1.1.0: resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk= +dom-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dom-iterator/-/dom-iterator-1.0.0.tgz#9c09899846ec41c2d257adc4d6015e4759ef05ad" + integrity sha512-7dsMOQI07EMU98gQM8NSB3GsAiIeBYIPKpnxR3c9xOvdvBjChAcOM0iJ222I3p5xyiZO9e5oggkNaCusuTdYig== + dependencies: + component-props "1.1.1" + component-xor "0.0.4" + domain-browser@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" @@ -2951,6 +3136,42 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.51, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: + version "0.10.53" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" + integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.3" + next-tick "~1.0.0" + +es6-iterator@^2.0.3, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + +es6-weak-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -2983,9 +3204,9 @@ eslint-config-google@^0.14.0: resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a" integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== -"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main": - version "0.3.3" - resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/50d6bdf6704dd95016d5f1f824f00cac6eaa64e1" +"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945": + version "0.3.5" + resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/2306b3d4da4eba908b256014b979f1d3d43d2945" eslint-rule-composer@^0.3.0: version "0.3.0" @@ -3119,6 +3340,14 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= + dependencies: + d "1" + es5-ext "~0.10.14" + events@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -3218,6 +3447,13 @@ expect@^26.6.2: jest-message-util "^26.6.2" jest-regex-util "^26.0.0" +ext@^1.1.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" + integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== + dependencies: + type "^2.0.0" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -4079,12 +4315,17 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-promise@^2.0.0: +is-promise@^2.0.0, is-promise@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== @@ -4643,6 +4884,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + js2xmlparser@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-4.0.1.tgz#670ef71bc5661f089cc90481b99a05a1227ae3bd" @@ -4949,6 +5197,20 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= + dependencies: + es5-ext "~0.10.2" + +magic-string@^0.25.1: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -4999,6 +5261,11 @@ markdown-it@^10.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" +marked@^1.1.1: + version "1.2.9" + resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.9.tgz#53786f8b05d4c01a2a5a76b7d1ec9943d29d72dc" + integrity sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw== + marked@^2.0.3: version "2.1.3" resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" @@ -5026,6 +5293,20 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= +memoizee@^0.4.14: + version "0.4.15" + resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72" + integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ== + dependencies: + d "^1.0.1" + es5-ext "^0.10.53" + es6-weak-map "^2.0.3" + event-emitter "^0.3.5" + is-promise "^2.2.2" + lru-queue "^0.1.0" + next-tick "^1.1.0" + timers-ext "^0.1.7" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -5211,6 +5492,16 @@ neo-async@^2.6.1: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +next-tick@1, next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +next-tick@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -5223,6 +5514,11 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" +node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -5406,6 +5702,11 @@ os-browserify@~0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= +os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + p-each-series@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" @@ -5638,6 +5939,11 @@ pretty-format@^26.0.0, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" +prism-react-renderer@^1.0.1, prism-react-renderer@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.2.1.tgz#392460acf63540960e5e3caa699d851264e99b89" + integrity sha512-w23ch4f75V1Tnz8DajsYKvY5lF7H1+WvzvLUcF0paFxkTHSp42RS0H5CttdN2Q8RR3DRGZ9v5xD/h3n8C8kGmg== + private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -5673,7 +5979,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.7.2: +prop-types@^15.5.8, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -5917,6 +6223,29 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-live@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-live/-/react-live-2.2.3.tgz#260f99194213799f0005e473e7a4154c699d6a7c" + integrity sha512-tpKruvfytNETuzO3o1mrQUj180GVrq35IE8F5gH1NJVPt4szYCx83/dOSCOyjgRhhc3gQvl0pQ3k/CjOjwJkKQ== + dependencies: + buble "0.19.6" + core-js "^2.4.1" + dom-iterator "^1.0.0" + prism-react-renderer "^1.0.1" + prop-types "^15.5.8" + react-simple-code-editor "^0.10.0" + unescape "^1.0.1" + +react-simple-code-editor@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.10.0.tgz#73e7ac550a928069715482aeb33ccba36efe2373" + integrity sha512-bL5W5mAxSW6+cLwqqVWY47Silqgy2DKDTR4hDBrLrUqC5BXc29YVx17l2IZk5v36VcDEq1Bszu2oHm1qBwKqBA== + +react-simple-code-editor@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.11.0.tgz#bb57c7c29b570f2ab229872599eac184f5bc673c" + integrity sha512-xGfX7wAzspl113ocfKQAR8lWPhavGWHL3xSzNLeseDRHysT+jzRBi/ExdUqevSMos+7ZtdfeuBOXtgk9HTwsrw== + read-only-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" @@ -6054,7 +6383,7 @@ regexpp@^3.1.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -regexpu-core@^4.7.1: +regexpu-core@^4.2.0, regexpu-core@^4.7.1: version "4.7.1" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== @@ -6474,6 +6803,11 @@ source-map@^0.7.3, source-map@~0.7.2: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + spdx-correct@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" @@ -6788,6 +7122,14 @@ timers-browserify@^1.0.1: dependencies: process "~0.11.0" +timers-ext@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" + integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== + dependencies: + es5-ext "~0.10.46" + next-tick "1" + tmatch@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf" @@ -6964,6 +7306,16 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.0.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d" + integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -6976,7 +7328,7 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.2.2, typescript@^3.7.5: +typescript@^3.2.2: version "3.9.10" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== @@ -7056,6 +7408,13 @@ underscore@^1.9.1, underscore@~1.13.1: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== +unescape@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unescape/-/unescape-1.0.1.tgz#956e430f61cad8a4d57d82c518f5e6cc5d0dda96" + integrity sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ== + dependencies: + extend-shallow "^2.0.1" + unhomoglyph@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.6.tgz#ea41f926d0fcf598e3b8bb2980c2ddac66b081d3" @@ -7094,6 +7453,11 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -7211,6 +7575,11 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vlq@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" + integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== + vm-browserify@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" @@ -7523,6 +7892,19 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb" + integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"