From 3d8b2ba87ce189b22886434492ba707ae0ff8096 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 27 Jun 2022 17:46:58 -0400 Subject: [PATCH 01/21] WIP RoomWidgetClient --- package.json | 1 + src/embedded.ts | 128 +++++++++++++++++++++++++++++++++++++++++++++ src/matrix.ts | 39 +++++++++----- src/sync.ts | 15 +++--- src/webrtc/call.ts | 39 +++++++------- yarn.lock | 15 +++++- 6 files changed, 195 insertions(+), 42 deletions(-) create mode 100644 src/embedded.ts diff --git a/package.json b/package.json index 0cd570a1459..4a06f45e5e0 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "content-type": "^1.0.4", "loglevel": "^1.7.1", "matrix-events-sdk": "^0.0.1-beta.7", + "matrix-widget-api": "^0.1.0-beta.18", "p-retry": "^4.5.0", "qs": "^6.9.6", "request": "^2.88.2", diff --git a/src/embedded.ts b/src/embedded.ts new file mode 100644 index 00000000000..2d8b5bed5f7 --- /dev/null +++ b/src/embedded.ts @@ -0,0 +1,128 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { + WidgetApi, + WidgetApiToWidgetAction, + ISendEventToWidgetActionRequest, + ISendToDeviceToWidgetActionRequest, +} from "matrix-widget-api"; + +import { ISendEventResponse } from "./@types/requests"; +import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts } from "./client"; +import { MatrixEvent } from "./models/event"; +import { Room } from "./models/room"; + +interface IStateEventRequest { + eventType: string; + stateKey?: string; +} + +export interface IEventRequests { + // TODO: Add fields for requesting event types and message types + + sendState?: IStateEventRequest[]; + receiveState?: IStateEventRequest[]; + + sendToDevice?: string[]; + receiveToDevice?: string[]; +} + +export class RoomWidgetClient extends MatrixClient { + private room: Room; + + constructor( + private readonly widgetApi: WidgetApi, + private readonly eventRequests: IEventRequests, + private readonly roomId: string, + opts: IMatrixClientCreateOpts, + ) { + super(opts); + + // Request capabilities for the events we want to send/receive + this.eventRequests.sendState?.forEach(({ eventType, stateKey }) => + this.widgetApi.requestCapabilityToSendState(eventType, stateKey), + ); + this.eventRequests.receiveState?.forEach(({ eventType, stateKey }) => + this.widgetApi.requestCapabilityToReceiveState(eventType, stateKey), + ); + this.eventRequests.sendToDevice?.forEach(eventType => + this.widgetApi.requestCapabilityToSendToDevice(eventType), + ); + this.eventRequests.receiveToDevice?.forEach(eventType => + this.widgetApi.requestCapabilityToReceiveToDevice(eventType), + ); + + this.widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent); + this.widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); + + // Open communication with the host + this.widgetApi.start(); + } + + public async startClient(opts?: IStartClientOpts): Promise { + await super.startClient(opts); + + this.room = this.syncApi.createRoom(this.roomId); + this.store.storeRoom(this.room); + window.mxRoom = this.room; + + // Backfill the requested events + await Promise.all( + this.eventRequests.receiveState?.map(async ({ eventType, stateKey }) => { + const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey); + const events = rawEvents.map(rawEvent => new MatrixEvent(rawEvent)); + + await this.syncApi.injectRoomEvents(this.room, [], events); + events.forEach(event => this.emit(ClientEvent.Event, event)); + console.log("injected", events); + }) ?? [], + ); + } + + public stopClient() { + this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent); + this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); + + super.stopClient(); + } + + public async sendStateEvent( + roomId: string, + eventType: string, + content: any, + stateKey = "", + ): Promise { + return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId); + } + + public async sendToDevice( + eventType: string, + contentMap: { [userId: string]: { [deviceId: string]: Record } }, + ): Promise<{}> { + await this.widgetApi.sendToDevice(eventType, contentMap); + return {}; + } + + private onEvent = async (ev: CustomEvent) => { + const event = new MatrixEvent(ev.detail.data); + await this.syncApi.injectRoomEvents(this.room, [], [event]); + this.emit(ClientEvent.Event, event); + }; + + private onToDevice = (ev: CustomEvent) => + this.emit(ClientEvent.ToDeviceEvent, new MatrixEvent(ev.detail.data)); +} diff --git a/src/matrix.ts b/src/matrix.ts index 646f8879818..f50f1995573 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -14,10 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { WidgetApi } from "matrix-widget-api"; + import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store"; import { MemoryStore } from "./store/memory"; import { MatrixScheduler } from "./scheduler"; import { MatrixClient, ICreateClientOpts } from "./client"; +import { RoomWidgetClient, IEventRequests } from "./embedded"; import { DeviceTrustLevel } from "./crypto/CrossSigning"; import { ISecretStorageKeyInfo } from "./crypto/api"; @@ -131,6 +134,19 @@ export interface ICryptoCallbacks { getBackupKey?: () => Promise; } +function amendClientOpts(opts: ICreateClientOpts | string): ICreateClientOpts { + if (typeof opts === "string") opts = { baseUrl: opts }; + + opts.request = opts.request ?? requestInstance; + opts.store = opts.store ?? new MemoryStore({ + localStorage: global.localStorage, + }); + opts.scheduler = opts.scheduler ?? new MatrixScheduler(); + opts.cryptoStore = opts.cryptoStore ?? cryptoStoreFactory(); + + return opts; +} + /** * Construct a Matrix Client. Similar to {@link module:client.MatrixClient} * except that the 'request', 'store' and 'scheduler' dependencies are satisfied. @@ -154,19 +170,16 @@ export interface ICryptoCallbacks { * @see {@link module:client.MatrixClient} for the full list of options for * opts. */ -export function createClient(opts: ICreateClientOpts | string) { - if (typeof opts === "string") { - opts = { - "baseUrl": opts, - }; - } - opts.request = opts.request || requestInstance; - opts.store = opts.store || new MemoryStore({ - localStorage: global.localStorage, - }); - opts.scheduler = opts.scheduler || new MatrixScheduler(); - opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory(); - return new MatrixClient(opts); +export function createClient(opts: ICreateClientOpts | string): MatrixClient { + return new MatrixClient(amendClientOpts(opts)); +} +export function createRoomWidgetClient( + widgetApi: WidgetApi, + eventRequests: IEventRequests, + roomId: string, + opts: ICreateClientOpts | string, +): MatrixClient { + return new RoomWidgetClient(widgetApi, eventRequests, roomId, amendClientOpts(opts)); } /** diff --git a/src/sync.ts b/src/sync.ts index 4abd4fb5bb2..3e124082130 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -398,7 +398,7 @@ export class SyncApi { // events so that clients can start back-paginating. room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS); - await this.processRoomEvents(room, stateEvents, events); + await this.injectRoomEvents(room, stateEvents, events); room.recalculate(); client.store.storeRoom(room); @@ -430,7 +430,7 @@ export class SyncApi { response.messages.chunk = response.messages.chunk || []; response.state = response.state || []; - // FIXME: Mostly duplicated from processRoomEvents but not entirely + // FIXME: Mostly duplicated from injectRoomEvents but not entirely // because "state" in this API is at the BEGINNING of the chunk const oldStateEvents = utils.deepCopy(response.state) .map(client.getEventMapper()); @@ -1252,7 +1252,7 @@ export class SyncApi { const room = inviteObj.room; const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room); - await this.processRoomEvents(room, stateEvents); + await this.injectRoomEvents(room, stateEvents); if (inviteObj.isBrandNewRoom) { room.recalculate(); client.store.storeRoom(room); @@ -1358,7 +1358,7 @@ export class SyncApi { } } - await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache); + await this.injectRoomEvents(room, stateEvents, events, syncEventData.fromCache); // set summary after processing events, // because it will trigger a name calculation @@ -1410,7 +1410,7 @@ export class SyncApi { const events = this.mapSyncEventsFormat(leaveObj.timeline, room); const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data); - await this.processRoomEvents(room, stateEvents, events); + await this.injectRoomEvents(room, stateEvents, events); room.addAccountData(accountDataEvents); room.recalculate(); @@ -1649,14 +1649,15 @@ export class SyncApi { } /** + * Injects events into a room's model. * @param {Room} room * @param {MatrixEvent[]} stateEventList A list of state events. This is the state * at the *START* of the timeline list if it is supplied. * @param {MatrixEvent[]} [timelineEventList] A list of timeline events, including threaded. Lower index - * @param {boolean} fromCache whether the sync response came from cache * is earlier in time. Higher index is later. + * @param {boolean} fromCache whether the sync response came from cache */ - private async processRoomEvents( + public async injectRoomEvents( room: Room, stateEventList: MatrixEvent[], timelineEventList?: MatrixEvent[], diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 438a659008b..f3868bc755c 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -2070,36 +2070,33 @@ export class MatrixCall extends TypedEventEmitter Date: Fri, 15 Jul 2022 14:36:01 -0400 Subject: [PATCH 02/21] Wait for the widget API to become ready before backfilling --- src/embedded.ts | 15 ++++++++++++--- src/webrtc/call.ts | 12 ++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/embedded.ts b/src/embedded.ts index 2d8b5bed5f7..fa1f81162c1 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -22,6 +22,7 @@ import { } from "matrix-widget-api"; import { ISendEventResponse } from "./@types/requests"; +import { logger } from "./logger"; import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts } from "./client"; import { MatrixEvent } from "./models/event"; import { Room } from "./models/room"; @@ -43,6 +44,7 @@ export interface IEventRequests { export class RoomWidgetClient extends MatrixClient { private room: Room; + private widgetApiReady = new Promise(resolve => this.widgetApi.once("ready", resolve)); constructor( private readonly widgetApi: WidgetApi, @@ -78,19 +80,25 @@ export class RoomWidgetClient extends MatrixClient { this.room = this.syncApi.createRoom(this.roomId); this.store.storeRoom(this.room); - window.mxRoom = this.room; + + await this.widgetApiReady; // Backfill the requested events + // We only get the most recent event for every type + state key combo, + // so it doesn't really matter what order we inject them in await Promise.all( this.eventRequests.receiveState?.map(async ({ eventType, stateKey }) => { const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey); const events = rawEvents.map(rawEvent => new MatrixEvent(rawEvent)); await this.syncApi.injectRoomEvents(this.room, [], events); - events.forEach(event => this.emit(ClientEvent.Event, event)); - console.log("injected", events); + events.forEach(event => { + this.emit(ClientEvent.Event, event); + logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); + }); }) ?? [], ); + logger.info("Finished backfilling events"); } public stopClient() { @@ -121,6 +129,7 @@ export class RoomWidgetClient extends MatrixClient { const event = new MatrixEvent(ev.detail.data); await this.syncApi.injectRoomEvents(this.room, [], [event]); this.emit(ClientEvent.Event, event); + logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); }; private onToDevice = (ev: CustomEvent) => diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 7eb63afd1a9..695e8208700 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -2181,7 +2181,7 @@ export class MatrixCall extends TypedEventEmitter Date: Fri, 15 Jul 2022 15:59:54 -0400 Subject: [PATCH 03/21] Add support for sending user-defined encrypted to-device messages This is a port of the same change from the robertlong/group-call branch. --- src/crypto/algorithms/megolm.ts | 97 ++++---------------------- src/crypto/index.ts | 117 ++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 83 deletions(-) diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index c16187fd622..8af74abc181 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -609,93 +609,24 @@ class MegolmEncryption extends EncryptionAlgorithm { userDeviceMap: IOlmDevice[], payload: IPayload, ): Promise { - const contentMap: Record> = {}; - // Map from userId to a map of deviceId to deviceInfo - const deviceInfoByUserIdAndDeviceId = new Map>(); - - const promises: Promise[] = []; - for (let i = 0; i < userDeviceMap.length; i++) { - const encryptedContent: IEncryptedContent = { - algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, - ciphertext: {}, - }; - const val = userDeviceMap[i]; - const userId = val.userId; - const deviceInfo = val.deviceInfo; - const deviceId = deviceInfo.deviceId; - - // Assign to temp value to make type-checking happy - let userIdDeviceInfo = deviceInfoByUserIdAndDeviceId.get(userId); - - if (userIdDeviceInfo === undefined) { - userIdDeviceInfo = new Map(); - - deviceInfoByUserIdAndDeviceId.set(userId, userIdDeviceInfo); - } - - // We hold by reference, this updates deviceInfoByUserIdAndDeviceId[userId] - userIdDeviceInfo.set(deviceId, deviceInfo); - - if (!contentMap[userId]) { - contentMap[userId] = {}; - } - contentMap[userId][deviceId] = encryptedContent; - - promises.push( - olmlib.encryptMessageForDevice( - encryptedContent.ciphertext, - this.userId, - this.deviceId, - this.olmDevice, - userId, - deviceInfo, - payload, - ), - ); - } - - return Promise.all(promises).then(() => { - // prune out any devices that encryptMessageForDevice could not encrypt for, - // in which case it will have just not added anything to the ciphertext object. - // There's no point sending messages to devices if we couldn't encrypt to them, - // since that's effectively a blank message. + return this.crypto.encryptAndSendToDevices( + userDeviceMap, + payload, + ).then(({ contentMap, deviceInfoByUserIdAndDeviceId }) => { + // store that we successfully uploaded the keys of the current slice for (const userId of Object.keys(contentMap)) { for (const deviceId of Object.keys(contentMap[userId])) { - if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) { - logger.log( - "No ciphertext for device " + - userId + ":" + deviceId + ": pruning", - ); - delete contentMap[userId][deviceId]; - } - } - // No devices left for that user? Strip that too. - if (Object.keys(contentMap[userId]).length === 0) { - logger.log("Pruned all devices for user " + userId); - delete contentMap[userId]; + session.markSharedWithDevice( + userId, + deviceId, + deviceInfoByUserIdAndDeviceId.get(userId).get(deviceId).getIdentityKey(), + chainIndex, + ); } } - - // Is there anything left? - if (Object.keys(contentMap).length === 0) { - logger.log("No users left to send to: aborting"); - return; - } - - return this.baseApis.sendToDevice("m.room.encrypted", contentMap).then(() => { - // store that we successfully uploaded the keys of the current slice - for (const userId of Object.keys(contentMap)) { - for (const deviceId of Object.keys(contentMap[userId])) { - session.markSharedWithDevice( - userId, - deviceId, - deviceInfoByUserIdAndDeviceId.get(userId).get(deviceId).getIdentityKey(), - chainIndex, - ); - } - } - }); + }).catch((error) => { + logger.error("failed to encryptAndSendToDevices", error); + throw error; }); } diff --git a/src/crypto/index.ts b/src/crypto/index.ts index afa5c4b8a6d..99a8ea8a7bd 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -26,6 +26,7 @@ import anotherjson from "another-json"; import { TypedReEmitter } from '../ReEmitter'; import { logger } from '../logger'; import { IExportedDevice, OlmDevice } from "./OlmDevice"; +import { IOlmDevice } from "./algorithms/megolm"; import * as olmlib from "./olmlib"; import { DeviceInfoMap, DeviceList } from "./DeviceList"; import { DeviceInfo, IDevice } from "./deviceinfo"; @@ -201,6 +202,19 @@ export interface IRequestsMap { setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void; } +/* eslint-disable camelcase */ +export interface IEncryptedContent { + algorithm: string; + sender_key: string; + ciphertext: Record; +} +/* eslint-enable camelcase */ + +interface IEncryptAndSendToDevicesResult { + contentMap: Record>; + deviceInfoByUserIdAndDeviceId: Map>; +} + export enum CryptoEvent { DeviceVerificationChanged = "deviceVerificationChanged", UserTrustStatusChanged = "userTrustStatusChanged", @@ -3097,6 +3111,109 @@ export class Crypto extends TypedEventEmitter} Promise which + * resolves once the message has been encrypted and sent to the given + * userDeviceMap, and returns the { contentMap, deviceInfoByDeviceId } + * of the successfully sent messages. + */ + public encryptAndSendToDevices( + userDeviceInfoArr: IOlmDevice[], + payload: object, + ): Promise { + const contentMap: Record> = {}; + const deviceInfoByUserIdAndDeviceId = new Map>(); + + const promises: Promise[] = []; + for (const { userId, deviceInfo } of userDeviceInfoArr) { + const deviceId = deviceInfo.deviceId; + const encryptedContent: IEncryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this.olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + + // Assign to temp value to make type-checking happy + let userIdDeviceInfo = deviceInfoByUserIdAndDeviceId.get(userId); + + if (userIdDeviceInfo === undefined) { + userIdDeviceInfo = new Map(); + deviceInfoByUserIdAndDeviceId.set(userId, userIdDeviceInfo); + } + + // We hold by reference, this updates deviceInfoByUserIdAndDeviceId[userId] + userIdDeviceInfo.set(deviceId, deviceInfo); + + if (!contentMap[userId]) { + contentMap[userId] = {}; + } + contentMap[userId][deviceId] = encryptedContent; + + promises.push( + olmlib.ensureOlmSessionsForDevices( + this.olmDevice, + this.baseApis, + { [userId]: [deviceInfo] }, + ).then(() => + olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this.userId, + this.deviceId, + this.olmDevice, + userId, + deviceInfo, + payload, + ), + ), + ); + } + + return Promise.all(promises).then(() => { + // prune out any devices that encryptMessageForDevice could not encrypt for, + // in which case it will have just not added anything to the ciphertext object. + // There's no point sending messages to devices if we couldn't encrypt to them, + // since that's effectively a blank message. + for (const userId of Object.keys(contentMap)) { + for (const deviceId of Object.keys(contentMap[userId])) { + if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) { + logger.log(`No ciphertext for device ${userId}:${deviceId}: pruning`); + delete contentMap[userId][deviceId]; + } + } + // No devices left for that user? Strip that too. + if (Object.keys(contentMap[userId]).length === 0) { + logger.log(`Pruned all devices for user ${userId}`); + delete contentMap[userId]; + } + } + + // Is there anything left? + if (Object.keys(contentMap).length === 0) { + logger.log("No users left to send to: aborting"); + return; + } + + return this.baseApis.sendToDevice("m.room.encrypted", contentMap).then( + (response) => ({ contentMap, deviceInfoByUserIdAndDeviceId }), + ).catch(error => { + logger.error("sendToDevice failed", error); + throw error; + }); + }).catch(error => { + logger.error("encryptAndSendToDevices promises failed", error); + throw error; + }); + } + private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => { try { this.onRoomMembership(event, member, oldMembership); From 86d5943cb369dca182c600f8fd14a5178a687b0e Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 16:23:51 -0400 Subject: [PATCH 04/21] Fix tests --- spec/unit/crypto/algorithms/megolm.spec.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index 9bb401b4d84..dfa2943e8a4 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -323,6 +323,13 @@ describe("MegolmDecryption", function() { rotation_period_ms: rotationPeriodMs, }, }); + + // Splice the real method onto the mock object as megolm uses this method + // on the crypto class in order to encrypt / start sessions + mockCrypto.encryptAndSendToDevices = Crypto.prototype.encryptAndSendToDevices; + mockCrypto.olmDevice = olmDevice; + mockCrypto.baseApis = mockBaseApis; + mockRoom = { getEncryptionTargetMembers: jest.fn().mockReturnValue( [{ userId: "@alice:home.server" }], From 3082e3f6438af0fec4941b3aa15089b5a70cc095 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 18:14:52 -0400 Subject: [PATCH 05/21] Emit an event when the client receives TURN servers --- src/client.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/client.ts b/src/client.ts index 73ca5144ef0..2788663e6d3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,5 +1,5 @@ /* -Copyright 2015-2021 The Matrix.org Foundation C.I.C. +Copyright 2015-2022 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -502,7 +502,7 @@ interface ITurnServerResponse { ttl: number; } -interface ITurnServer { +export interface ITurnServer { urls: string[]; username: string; credential: string; @@ -787,6 +787,8 @@ export enum ClientEvent { DeleteRoom = "deleteRoom", SyncUnexpectedError = "sync.unexpectedError", ClientWellKnown = "WellKnown.client", + TurnServers = "turnServers", + TurnServersError = "turnServers.error", } type RoomEvents = RoomEvent.Name @@ -857,6 +859,8 @@ export type ClientEventHandlerMap = { [ClientEvent.DeleteRoom]: (roomId: string) => void; [ClientEvent.SyncUnexpectedError]: (error: Error) => void; [ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void; + [ClientEvent.TurnServers]: (servers: ITurnServer[]) => void; + [ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void; } & RoomEventHandlerMap & RoomStateEventHandlerMap & CryptoEventHandlerMap @@ -933,7 +937,7 @@ export class MatrixClient extends TypedEventEmitter; protected turnServers: ITurnServer[] = []; protected turnServersExpiry = 0; - protected checkTurnServersIntervalID: ReturnType; + protected checkTurnServersIntervalID: ReturnType | null = null; protected exportedOlmDeviceToImport: IOlmDevice; protected txnCtr = 0; protected mediaHandler = new MediaHandler(this); @@ -1220,6 +1224,8 @@ export class MatrixClient extends TypedEventEmitter { if (!this.canSupportVoip) { @@ -6327,17 +6337,21 @@ export class MatrixClient extends TypedEventEmitter Date: Tue, 26 Jul 2022 16:23:53 -0400 Subject: [PATCH 06/21] Expose the method in MatrixClient --- src/client.ts | 32 +++++++++++++++++++++++++++++--- src/crypto/index.ts | 2 +- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/client.ts b/src/client.ts index 73ca5144ef0..7d82ad41228 100644 --- a/src/client.ts +++ b/src/client.ts @@ -40,9 +40,11 @@ import { sleep } from './utils'; import { Direction, EventTimeline } from "./models/event-timeline"; import { IActionsObject, PushProcessor } from "./pushprocessor"; import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery"; +import { IEncryptAndSendToDevicesResult } from "./crypto"; import * as olmlib from "./crypto/olmlib"; import { decodeBase64, encodeBase64 } from "./crypto/olmlib"; -import { IExportedDevice as IOlmDevice } from "./crypto/OlmDevice"; +import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice"; +import { IOlmDevice } from "./crypto/algorithms/megolm"; import { TypedReEmitter } from './ReEmitter'; import { IRoomEncryption, RoomList } from './crypto/RoomList'; import { logger } from './logger'; @@ -206,7 +208,7 @@ const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes interface IExportedDevice { - olmDevice: IOlmDevice; + olmDevice: IExportedOlmDevice; userId: string; deviceId: string; } @@ -934,7 +936,7 @@ export class MatrixClient extends TypedEventEmitter; - protected exportedOlmDeviceToImport: IOlmDevice; + protected exportedOlmDeviceToImport: IExportedOlmDevice; protected txnCtr = 0; protected mediaHandler = new MediaHandler(this); protected pendingEventEncryption = new Map>(); @@ -2544,6 +2546,30 @@ export class MatrixClient extends TypedEventEmitter} Promise which + * resolves once the message has been encrypted and sent to the given + * userDeviceMap, and returns the { contentMap, deviceInfoByDeviceId } + * of the successfully sent messages. + */ + public encryptAndSendToDevices( + userDeviceInfoArr: IOlmDevice[], + payload: object, + ): Promise { + if (!this.crypto) { + throw new Error("End-to-End encryption disabled"); + } + return this.crypto.encryptAndSendToDevices(userDeviceInfoArr, payload); + } + /** * Forces the current outbound group session to be discarded such * that another one will be created next time an event is sent. diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 36fe4cb961d..46ec7dcabce 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -210,7 +210,7 @@ export interface IEncryptedContent { } /* eslint-enable camelcase */ -interface IEncryptAndSendToDevicesResult { +export interface IEncryptAndSendToDevicesResult { contentMap: Record>; deviceInfoByUserIdAndDeviceId: Map>; } From c671532cf590b9c8b48ece6a4ba59bb000df65e0 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 26 Jul 2022 17:27:41 -0400 Subject: [PATCH 07/21] Override the encryptAndSendToDevices method --- src/embedded.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/embedded.ts b/src/embedded.ts index fa1f81162c1..41446ce87ec 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -26,6 +26,9 @@ import { logger } from "./logger"; import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts } from "./client"; import { MatrixEvent } from "./models/event"; import { Room } from "./models/room"; +import { IEncryptAndSendToDevicesResult } from "./crypto"; +import { DeviceInfo } from "./crypto/deviceinfo"; +import { IOlmDevice } from "./crypto/algorithms/megolm"; interface IStateEventRequest { eventType: string; @@ -121,17 +124,36 @@ export class RoomWidgetClient extends MatrixClient { eventType: string, contentMap: { [userId: string]: { [deviceId: string]: Record } }, ): Promise<{}> { - await this.widgetApi.sendToDevice(eventType, contentMap); + await this.widgetApi.sendToDevice(eventType, false, contentMap); return {}; } + public async encryptAndSendToDevices( + userDeviceInfoArr: IOlmDevice[], + payload: object, + ): Promise { + const contentMap: { [userId: string]: { [deviceId: string]: unknown } } = {}; + for (const { userId, deviceInfo: { deviceId } } of userDeviceInfoArr) { + if (!contentMap[userId]) contentMap[userId] = {}; + contentMap[userId][deviceId] = payload; + } + + // Since encryption is handled entirely on the other side of the widget + // API, we can't actually return anything useful + await this.widgetApi.sendToDevice((payload as { type: string }).type, true, contentMap); + return { contentMap: {}, deviceInfoByUserIdAndDeviceId: new Map() }; + } + private onEvent = async (ev: CustomEvent) => { + ev.preventDefault(); const event = new MatrixEvent(ev.detail.data); await this.syncApi.injectRoomEvents(this.room, [], [event]); this.emit(ClientEvent.Event, event); logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); }; - private onToDevice = (ev: CustomEvent) => + private onToDevice = (ev: CustomEvent) => { + ev.preventDefault(); this.emit(ClientEvent.ToDeviceEvent, new MatrixEvent(ev.detail.data)); + }; } From 4f40f9e22c809b43686f1410dcd329b42f38f798 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 28 Jul 2022 16:28:54 -0400 Subject: [PATCH 08/21] Add support for TURN servers in embedded mode and make calls mostly work --- src/embedded.ts | 108 +++++++++++++++++++++++++++++---- src/matrix.ts | 6 +- src/webrtc/call.ts | 9 ++- src/webrtc/callEventHandler.ts | 2 + 4 files changed, 108 insertions(+), 17 deletions(-) diff --git a/src/embedded.ts b/src/embedded.ts index 41446ce87ec..2d5ffb9a5d5 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -17,6 +17,9 @@ limitations under the License. import { WidgetApi, WidgetApiToWidgetAction, + MatrixCapabilities, + IWidgetApiRequest, + IWidgetApiAcknowledgeResponseData, ISendEventToWidgetActionRequest, ISendToDeviceToWidgetActionRequest, } from "matrix-widget-api"; @@ -24,7 +27,10 @@ import { import { ISendEventResponse } from "./@types/requests"; import { logger } from "./logger"; import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts } from "./client"; +import { SyncApi, SyncState } from "./sync"; +import { SlidingSyncSdk } from "./sliding-sync-sdk"; import { MatrixEvent } from "./models/event"; +import { User } from "./models/user"; import { Room } from "./models/room"; import { IEncryptAndSendToDevicesResult } from "./crypto"; import { DeviceInfo } from "./crypto/deviceinfo"; @@ -35,41 +41,48 @@ interface IStateEventRequest { stateKey?: string; } -export interface IEventRequests { - // TODO: Add fields for requesting event types and message types +export interface ICapabilities { + // TODO: Add fields for messages and other non-state events sendState?: IStateEventRequest[]; receiveState?: IStateEventRequest[]; sendToDevice?: string[]; receiveToDevice?: string[]; + + turnServers?: boolean; } export class RoomWidgetClient extends MatrixClient { private room: Room; private widgetApiReady = new Promise(resolve => this.widgetApi.once("ready", resolve)); + private lifecycle: AbortController; + private syncState: SyncState | null = null; constructor( private readonly widgetApi: WidgetApi, - private readonly eventRequests: IEventRequests, + private readonly capabilities: ICapabilities, private readonly roomId: string, opts: IMatrixClientCreateOpts, ) { super(opts); - // Request capabilities for the events we want to send/receive - this.eventRequests.sendState?.forEach(({ eventType, stateKey }) => + // Request capabilities for the functionality this client needs to support + this.capabilities.sendState?.forEach(({ eventType, stateKey }) => this.widgetApi.requestCapabilityToSendState(eventType, stateKey), ); - this.eventRequests.receiveState?.forEach(({ eventType, stateKey }) => + this.capabilities.receiveState?.forEach(({ eventType, stateKey }) => this.widgetApi.requestCapabilityToReceiveState(eventType, stateKey), ); - this.eventRequests.sendToDevice?.forEach(eventType => + this.capabilities.sendToDevice?.forEach(eventType => this.widgetApi.requestCapabilityToSendToDevice(eventType), ); - this.eventRequests.receiveToDevice?.forEach(eventType => + this.capabilities.receiveToDevice?.forEach(eventType => this.widgetApi.requestCapabilityToReceiveToDevice(eventType), ); + if (this.capabilities.turnServers) { + this.widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers); + } this.widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent); this.widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); @@ -78,8 +91,24 @@ export class RoomWidgetClient extends MatrixClient { this.widgetApi.start(); } - public async startClient(opts?: IStartClientOpts): Promise { - await super.startClient(opts); + public async startClient(opts: IStartClientOpts = {}): Promise { + this.lifecycle = new AbortController(); + + // Create our own user object artificially (instead of waiting for sync) + // so it's always available, even if the user is not in any rooms etc. + const userId = this.getUserId(); + if (userId) { + this.store.storeUser(new User(userId)); + } + + // Even though we have no access token and cannot sync, the sync class + // still has some valuable helper methods that we make use of, so we + // instantiate it anyways + if (opts.slidingSync) { + this.syncApi = new SlidingSyncSdk(opts.slidingSync, this, opts); + } else { + this.syncApi = new SyncApi(this, opts); + } this.room = this.syncApi.createRoom(this.roomId); this.store.storeRoom(this.room); @@ -90,7 +119,7 @@ export class RoomWidgetClient extends MatrixClient { // We only get the most recent event for every type + state key combo, // so it doesn't really matter what order we inject them in await Promise.all( - this.eventRequests.receiveState?.map(async ({ eventType, stateKey }) => { + this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => { const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey); const events = rawEvents.map(rawEvent => new MatrixEvent(rawEvent)); @@ -101,7 +130,11 @@ export class RoomWidgetClient extends MatrixClient { }); }) ?? [], ); + this.setSyncState(SyncState.Prepared); logger.info("Finished backfilling events"); + + // Watch for TURN servers, if requested + if (this.capabilities.turnServers) this.watchTurnServers(); } public stopClient() { @@ -109,6 +142,7 @@ export class RoomWidgetClient extends MatrixClient { this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); super.stopClient(); + this.lifecycle.abort(); // Signal to other async tasks that the client has stopped } public async sendStateEvent( @@ -117,7 +151,8 @@ export class RoomWidgetClient extends MatrixClient { content: any, stateKey = "", ): Promise { - return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId); + if (roomId !== this.roomId) throw new Error(`Can't send events to ${roomId}`); + return await this.widgetApi.sendStateEvent(eventType, stateKey, content); } public async sendToDevice( @@ -144,16 +179,63 @@ export class RoomWidgetClient extends MatrixClient { return { contentMap: {}, deviceInfoByUserIdAndDeviceId: new Map() }; } + // Overridden since we get TURN servers automatically over the widget API, + // and this method would otherwise complain about missing an access token + public async checkTurnServers(): Promise { + return this.turnServers.length > 0; + } + + // Overridden since we 'sync' manually without the sync API + public getSyncState(): SyncState { + return this.syncState; + } + + private setSyncState(state: SyncState) { + const oldState = this.syncState; + this.syncState = state; + this.emit(ClientEvent.Sync, state, oldState); + } + + private async ack(ev: CustomEvent): Promise { + await this.widgetApi.transport.reply(ev.detail, {}); + } + private onEvent = async (ev: CustomEvent) => { ev.preventDefault(); const event = new MatrixEvent(ev.detail.data); await this.syncApi.injectRoomEvents(this.room, [], [event]); this.emit(ClientEvent.Event, event); + this.setSyncState(SyncState.Syncing); logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); + await this.ack(ev); }; - private onToDevice = (ev: CustomEvent) => { + private onToDevice = async (ev: CustomEvent) => { ev.preventDefault(); this.emit(ClientEvent.ToDeviceEvent, new MatrixEvent(ev.detail.data)); + this.setSyncState(SyncState.Syncing); + await this.ack(ev); + }; + + private async watchTurnServers() { + const servers = this.widgetApi.getTurnServers(); + const onClientStopped = () => servers.return(undefined); + this.lifecycle.signal.addEventListener("abort", onClientStopped); + + try { + for await (const server of servers) { + this.turnServers = [{ + urls: server.uris, + username: server.username, + credential: server.password, + }]; + this.emit(ClientEvent.TurnServers, this.turnServers); + logger.log(`Received TURN server: ${server.uris}`); + } + } catch (e) { + logger.warn("Error watching TURN servers", e); + } finally { + this.lifecycle.signal.removeEventListener("abort", onClientStopped); + } }; } diff --git a/src/matrix.ts b/src/matrix.ts index f50f1995573..bf3e3a3f31f 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -20,7 +20,7 @@ import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store"; import { MemoryStore } from "./store/memory"; import { MatrixScheduler } from "./scheduler"; import { MatrixClient, ICreateClientOpts } from "./client"; -import { RoomWidgetClient, IEventRequests } from "./embedded"; +import { RoomWidgetClient, ICapabilities } from "./embedded"; import { DeviceTrustLevel } from "./crypto/CrossSigning"; import { ISecretStorageKeyInfo } from "./crypto/api"; @@ -175,11 +175,11 @@ export function createClient(opts: ICreateClientOpts | string): MatrixClient { } export function createRoomWidgetClient( widgetApi: WidgetApi, - eventRequests: IEventRequests, + capabilities: ICapabilities, roomId: string, opts: ICreateClientOpts | string, ): MatrixClient { - return new RoomWidgetClient(widgetApi, eventRequests, roomId, amendClientOpts(opts)); + return new RoomWidgetClient(widgetApi, capabilities, roomId, amendClientOpts(opts)); } /** diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 695e8208700..1e695ef2177 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -546,6 +546,13 @@ export class MatrixCall extends TypedEventEmitter Date: Thu, 28 Jul 2022 23:37:19 -0400 Subject: [PATCH 09/21] Don't put unclonable objects into VoIP events RoomWidget clients were unable to send m.call.candidate events, because the candidate objects were not clonable for use with postMessage. Converting such objects to their canonical JSON form before attempting to send them over the wire solves this. --- src/webrtc/call.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index f7eff670b7c..64374919eab 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -1800,7 +1800,7 @@ export class MatrixCall extends TypedEventEmitter candidate.toJSON()); this.candidateSendQueue = []; ++this.candidateSendTries; - const content = { - candidates: candidates, - }; + const content = { candidates }; logger.debug(`Call ${this.callId} attempting to send ${candidates.length} candidates`); try { await this.sendVoipEvent(EventType.CallCandidates, content); From 8664be361f0ee5b3eff4ea69a54d555fb24e9fb1 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 29 Jul 2022 14:32:18 -0400 Subject: [PATCH 10/21] Fix types --- src/webrtc/call.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 64374919eab..14a6b6b2d4f 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -562,7 +562,7 @@ export class MatrixCall extends TypedEventEmitter candidate.toJSON()); + const candidates = this.candidateSendQueue; this.candidateSendQueue = []; ++this.candidateSendTries; - const content = { candidates }; + const content = { candidates: candidates.map(candidate => candidate.toJSON()) }; logger.debug(`Call ${this.callId} attempting to send ${candidates.length} candidates`); try { await this.sendVoipEvent(EventType.CallCandidates, content); From 52b755921685699e81b66245fd39e63e23f6456b Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 29 Jul 2022 14:38:06 -0400 Subject: [PATCH 11/21] Fix more types --- spec/unit/crypto/algorithms/megolm.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index 3e2acca379d..d232e714b02 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -360,8 +360,10 @@ describe("MegolmDecryption", function() { // Splice the real method onto the mock object as megolm uses this method // on the crypto class in order to encrypt / start sessions - mockCrypto.encryptAndSendToDevices = Crypto.prototype.encryptAndSendToDevices; + mockCrypto.encryptAndSendToDevices.mockImplementation(Crypto.prototype.encryptAndSendToDevices); + // @ts-ignore assigning to readonly prop mockCrypto.olmDevice = olmDevice; + // @ts-ignore assigning to readonly prop mockCrypto.baseApis = mockBaseApis; mockRoom = { From ca4db6e031bff8b9efc48b66faefb08f18ba0268 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 29 Jul 2022 14:38:23 -0400 Subject: [PATCH 12/21] Fix lint --- src/embedded.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/embedded.ts b/src/embedded.ts index 2d5ffb9a5d5..0acb9ac3146 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -237,5 +237,5 @@ export class RoomWidgetClient extends MatrixClient { } finally { this.lifecycle.signal.removeEventListener("abort", onClientStopped); } - }; + } } From afd8b558c58fc1894dc8d53a38051dfe91f5e184 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 5 Aug 2022 15:41:56 -0400 Subject: [PATCH 13/21] Upgrade matrix-widget-api --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f3726345ee6..4992a129027 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "content-type": "^1.0.4", "loglevel": "^1.7.1", "matrix-events-sdk": "^0.0.1-beta.7", - "matrix-widget-api": "^0.1.0-beta.18", + "matrix-widget-api": "^1.0.0", "p-retry": "4", "qs": "^6.9.6", "request": "^2.88.2", From 21682d2e855d135b25deec6d41209135a25572f2 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 5 Aug 2022 16:08:01 -0400 Subject: [PATCH 14/21] Save lockfile --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index e48fb888bde..c2553affe81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4818,10 +4818,10 @@ matrix-mock-request@^2.1.2: dependencies: expect "^28.1.0" -matrix-widget-api@^0.1.0-beta.18: - version "0.1.0-beta.18" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.18.tgz#4efd30edec3eeb4211285985464c062fcab59795" - integrity sha512-kCpcs6rrB94Mmr2/1gBJ+6auWyZ5UvOMOn5K2VFafz2/NDMzZg9OVWj9KFYnNAuwwBE5/tCztYEj6OQ+hgbwOQ== +matrix-widget-api@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.0.0.tgz#0cde6839cca66ad817ab12aca3490ccc8bac97d1" + integrity sha512-cy8p/8EteRPTFIAw7Q9EgPUJc2jD19ZahMR8bMKf2NkILDcjuPMC0UWnsJyB3fSnlGw+VbGepttRpULM31zX8Q== dependencies: "@types/events" "^3.0.0" events "^3.2.0" From 83361efacbf59c3c5c35bc4fa097706c05b9dc38 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 5 Aug 2022 22:00:29 -0400 Subject: [PATCH 15/21] Untangle dependencies to fix tests --- src/ToDeviceMessageQueue.ts | 2 +- src/client.ts | 46 +++++++++++++---------------- src/crypto/CrossSigning.ts | 2 +- src/crypto/EncryptionSetup.ts | 7 ++--- src/crypto/SecretStorage.ts | 5 ++-- src/crypto/index.ts | 23 +++++++++++++++ src/matrix.ts | 30 +++---------------- src/models/beacon.ts | 2 +- src/models/thread.ts | 7 +++-- src/sliding-sync-sdk.ts | 3 +- src/utils.ts | 2 +- src/webrtc/groupCallEventHandler.ts | 4 +-- 12 files changed, 66 insertions(+), 67 deletions(-) diff --git a/src/ToDeviceMessageQueue.ts b/src/ToDeviceMessageQueue.ts index 12827d8bbc8..66f471c415d 100644 --- a/src/ToDeviceMessageQueue.ts +++ b/src/ToDeviceMessageQueue.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { logger } from "./logger"; -import { MatrixClient } from "./matrix"; +import { MatrixClient } from "./client"; import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage"; import { MatrixScheduler } from "./scheduler"; diff --git a/src/client.ts b/src/client.ts index 45e1cd17318..0aa83d7402c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -72,6 +72,7 @@ import { CryptoEvent, CryptoEventHandlerMap, fixBackupKey, + ICryptoCallbacks, IBootstrapCrossSigningOpts, ICheckOwnCrossSigningTrustOpts, IMegolmSessionData, @@ -101,29 +102,9 @@ import { } from "./crypto/keybackup"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import { MatrixScheduler } from "./scheduler"; -import { - IAuthData, - ICryptoCallbacks, - IMinimalEvent, - IRoomEvent, - IStateEvent, - NotificationCountType, - BeaconEvent, - BeaconEventHandlerMap, - RoomEvent, - RoomEventHandlerMap, - RoomMemberEvent, - RoomMemberEventHandlerMap, - RoomStateEvent, - RoomStateEventHandlerMap, - INotificationsResponse, - IFilterResponse, - ITagsResponse, - IStatusResponse, - IPushRule, - PushRuleActionName, - IAuthDict, -} from "./matrix"; +import { BeaconEvent, BeaconEventHandlerMap } from "./models/beacon"; +import { IAuthData, IAuthDict } from "./interactive-auth"; +import { IMinimalEvent, IRoomEvent, IStateEvent } from "./sync-accumulator"; import { CrossSigningKey, IAddSecretStorageKeyOpts, @@ -138,7 +119,9 @@ import { VerificationRequest } from "./crypto/verification/request/VerificationR import { VerificationBase as Verification } from "./crypto/verification/Base"; import * as ContentHelpers from "./content-helpers"; import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning"; -import { Room } from "./models/room"; +import { Room, NotificationCountType, RoomEvent, RoomEventHandlerMap } from "./models/room"; +import { RoomMemberEvent, RoomMemberEventHandlerMap } from "./models/room-member"; +import { RoomStateEvent, RoomStateEventHandlerMap } from "./models/room-state"; import { IAddThreePidOnlyBody, IBindThreePidBody, @@ -156,6 +139,10 @@ import { ISearchOpts, ISendEventResponse, IUploadOpts, + INotificationsResponse, + IFilterResponse, + ITagsResponse, + IStatusResponse, } from "./@types/requests"; import { EventType, @@ -185,7 +172,16 @@ import { } from "./@types/search"; import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@types/synapse"; import { IHierarchyRoom } from "./@types/spaces"; -import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules"; +import { + IPusher, + IPusherRequest, + IPushRule, + IPushRules, + PushRuleAction, + PushRuleActionName, + PushRuleKind, + RuleId, +} from "./@types/PushRules"; import { IThreepid } from "./@types/threepids"; import { CryptoStore } from "./crypto/store/base"; import { diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index 4e5c9f452ec..6121d45fdd6 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -29,7 +29,7 @@ import { DeviceInfo } from "./deviceinfo"; import { SecretStorage } from "./SecretStorage"; import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client"; import { OlmDevice } from "./OlmDevice"; -import { ICryptoCallbacks } from "../matrix"; +import { ICryptoCallbacks } from "."; import { ISignatures } from "../@types/signed"; import { CryptoStore } from "./store/base"; import { ISecretStorageKeyInfo } from "./api"; diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index 61ba34eaf99..456c1cac4e0 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -19,16 +19,15 @@ import { MatrixEvent } from "../models/event"; import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning"; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { Method, PREFIX_UNSTABLE } from "../http-api"; -import { Crypto, IBootstrapCrossSigningOpts } from "./index"; +import { Crypto, ICryptoCallbacks, IBootstrapCrossSigningOpts } from "./index"; import { ClientEvent, - CrossSigningKeys, ClientEventHandlerMap, + CrossSigningKeys, ICrossSigningKey, - ICryptoCallbacks, ISignedKey, KeySignatures, -} from "../matrix"; +} from "../client"; import { ISecretStorageKeyInfo } from "./api"; import { IKeyBackupInfo } from "./keybackup"; import { TypedEventEmitter } from "../models/typed-event-emitter"; diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index 0eef2ee7d50..09bf467bb31 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -19,8 +19,9 @@ import * as olmlib from './olmlib'; import { encodeBase64 } from './olmlib'; import { randomString } from '../randomstring'; import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes'; -import { ClientEvent, ICryptoCallbacks, MatrixEvent } from '../matrix'; -import { ClientEventHandlerMap, MatrixClient } from "../client"; +import { ICryptoCallbacks } from "."; +import { MatrixEvent } from "../models/event"; +import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client"; import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api'; import { TypedEventEmitter } from '../models/typed-event-emitter'; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 451d2d8c82a..e6d47378623 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -126,6 +126,29 @@ export interface IBootstrapCrossSigningOpts { authUploadDeviceSigningKeys?(makeRequest: (authData: any) => Promise<{}>): Promise; } +export interface ICryptoCallbacks { + getCrossSigningKey?: (keyType: string, pubKey: string) => Promise; + saveCrossSigningKeys?: (keys: Record) => void; + shouldUpgradeDeviceVerifications?: ( + users: Record + ) => Promise; + getSecretStorageKey?: ( + keys: {keys: Record}, name: string + ) => Promise<[string, Uint8Array] | null>; + cacheSecretStorageKey?: ( + keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array + ) => void; + onSecretRequested?: ( + userId: string, deviceId: string, + requestId: string, secretName: string, deviceTrust: DeviceTrustLevel + ) => Promise; + getDehydrationKey?: ( + keyInfo: ISecretStorageKeyInfo, + checkFunc: (key: Uint8Array) => void, + ) => Promise; + getBackupKey?: () => Promise; +} + /* eslint-disable camelcase */ interface IRoomKey { room_id: string; diff --git a/src/matrix.ts b/src/matrix.ts index bf3e3a3f31f..2faead9f561 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -1,5 +1,5 @@ /* -Copyright 2015-2021 The Matrix.org Foundation C.I.C. +Copyright 2015-2022 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,10 +21,9 @@ import { MemoryStore } from "./store/memory"; import { MatrixScheduler } from "./scheduler"; import { MatrixClient, ICreateClientOpts } from "./client"; import { RoomWidgetClient, ICapabilities } from "./embedded"; -import { DeviceTrustLevel } from "./crypto/CrossSigning"; -import { ISecretStorageKeyInfo } from "./crypto/api"; export * from "./client"; +export * from "./embedded"; export * from "./http-api"; export * from "./autodiscovery"; export * from "./sync-accumulator"; @@ -54,6 +53,7 @@ export * from './@types/requests'; export * from './@types/search'; export * from './models/room-summary'; export * as ContentHelpers from "./content-helpers"; +export { ICryptoCallbacks } from "./crypto"; // used to be located here export { createNewMatrixCall } from "./webrtc/call"; export type { MatrixCall } from "./webrtc/call"; export { @@ -111,29 +111,6 @@ export function setCryptoStoreFactory(fac) { cryptoStoreFactory = fac; } -export interface ICryptoCallbacks { - getCrossSigningKey?: (keyType: string, pubKey: string) => Promise; - saveCrossSigningKeys?: (keys: Record) => void; - shouldUpgradeDeviceVerifications?: ( - users: Record - ) => Promise; - getSecretStorageKey?: ( - keys: {keys: Record}, name: string - ) => Promise<[string, Uint8Array] | null>; - cacheSecretStorageKey?: ( - keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array - ) => void; - onSecretRequested?: ( - userId: string, deviceId: string, - requestId: string, secretName: string, deviceTrust: DeviceTrustLevel - ) => Promise; - getDehydrationKey?: ( - keyInfo: ISecretStorageKeyInfo, - checkFunc: (key: Uint8Array) => void, - ) => Promise; - getBackupKey?: () => Promise; -} - function amendClientOpts(opts: ICreateClientOpts | string): ICreateClientOpts { if (typeof opts === "string") opts = { baseUrl: opts }; @@ -173,6 +150,7 @@ function amendClientOpts(opts: ICreateClientOpts | string): ICreateClientOpts { export function createClient(opts: ICreateClientOpts | string): MatrixClient { return new MatrixClient(amendClientOpts(opts)); } + export function createRoomWidgetClient( widgetApi: WidgetApi, capabilities: ICapabilities, diff --git a/src/models/beacon.ts b/src/models/beacon.ts index 9df62bbe2b1..12db328b11f 100644 --- a/src/models/beacon.ts +++ b/src/models/beacon.ts @@ -17,7 +17,7 @@ limitations under the License. import { MBeaconEventContent } from "../@types/beacon"; import { M_TIMESTAMP } from "../@types/location"; import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers"; -import { MatrixEvent } from "../matrix"; +import { MatrixEvent } from "./event"; import { sortEventsByLatestContentTimestamp } from "../utils"; import { TypedEventEmitter } from "./typed-event-emitter"; diff --git a/src/models/thread.ts b/src/models/thread.ts index c451ccb8dc2..a68b88c0417 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -16,13 +16,14 @@ limitations under the License. import { Optional } from "matrix-events-sdk"; -import { MatrixClient, MatrixEventEvent, RelationType, RoomEvent } from "../matrix"; +import { MatrixClient } from "../client"; import { TypedReEmitter } from "../ReEmitter"; import { IRelationsRequestOpts } from "../@types/requests"; -import { IThreadBundledRelationship, MatrixEvent } from "./event"; +import { RelationType } from "../@types/event"; +import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "./event"; import { Direction, EventTimeline } from "./event-timeline"; import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set'; -import { Room } from './room'; +import { Room, RoomEvent } from './room'; import { TypedEventEmitter } from "./typed-event-emitter"; import { RoomState } from "./room-state"; import { ServerControlledNamespacedValue } from "../NamespacedValue"; diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 4ebf5a4500a..e2dfcc381c0 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -35,7 +35,8 @@ import { SlidingSyncEvent, SlidingSyncState, } from "./sliding-sync"; -import { EventType, IPushRules } from "./matrix"; +import { EventType } from "./@types/event"; +import { IPushRules } from "./@types/PushRules"; import { PushProcessor } from "./pushprocessor"; // Number of consecutive failed syncs that will lead to a syncState of ERROR as opposed diff --git a/src/utils.ts b/src/utils.ts index 6cf459097c1..18d6c29a0fb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -24,7 +24,7 @@ import unhomoglyph from "unhomoglyph"; import promiseRetry from "p-retry"; import type * as NodeCrypto from "crypto"; -import { MatrixEvent } from "."; +import { MatrixEvent } from "./models/event"; import { M_TIMESTAMP } from "./@types/location"; /** diff --git a/src/webrtc/groupCallEventHandler.ts b/src/webrtc/groupCallEventHandler.ts index b30f0dd8a1a..9d7f2e5afd4 100644 --- a/src/webrtc/groupCallEventHandler.ts +++ b/src/webrtc/groupCallEventHandler.ts @@ -16,7 +16,7 @@ limitations under the License. import { MatrixEvent } from '../models/event'; import { RoomStateEvent } from '../models/room-state'; -import { MatrixClient } from '../client'; +import { MatrixClient, ClientEvent } from '../client'; import { GroupCall, GroupCallIntent, @@ -25,9 +25,9 @@ import { } from "./groupCall"; import { Room } from "../models/room"; import { RoomState } from "../models/room-state"; +import { RoomMember } from "../models/room-member"; import { logger } from '../logger'; import { EventType } from "../@types/event"; -import { ClientEvent, RoomMember } from '../matrix'; export enum GroupCallEventHandlerEvent { Incoming = "GroupCall.incoming", From 1491ce99b7cec5df1a49c2d3311710c301159e7b Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 6 Aug 2022 00:57:56 -0400 Subject: [PATCH 16/21] Add some preliminary tests --- package.json | 1 + spec/unit/embedded.spec.ts | 161 +++++++++++++++++ src/embedded.ts | 1 + yarn.lock | 355 +++++++++++++++++++++++++++++++++++-- 4 files changed, 502 insertions(+), 16 deletions(-) create mode 100644 spec/unit/embedded.spec.ts diff --git a/package.json b/package.json index 4ddd9bdbbdc..bc6ab9b7075 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "exorcist": "^2.0.0", "fake-indexeddb": "^4.0.0", "jest": "^28.0.0", + "jest-environment-jsdom": "^28.1.3", "jest-localstorage-mock": "^2.4.6", "jest-sonar-reporter": "^2.0.0", "jsdoc": "^3.6.6", diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts new file mode 100644 index 00000000000..5226b5f4e9f --- /dev/null +++ b/spec/unit/embedded.spec.ts @@ -0,0 +1,161 @@ +/** + * @jest-environment jsdom + */ + +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// We have to use EventEmitter here to mock part of the matrix-widget-api +// project, which doesn't know about our TypeEventEmitter implementation at all +// eslint-disable-next-line no-restricted-imports +import { EventEmitter } from "events"; +import { MockedObject } from "jest-mock"; +import { WidgetApi, WidgetApiToWidgetAction } from "matrix-widget-api"; + +import { createRoomWidgetClient } from "../../src/matrix"; +import { MatrixClient, ClientEvent } from "../../src/client"; +import { SyncState } from "../../src/sync"; +import { ICapabilities } from "../../src/embedded"; +import { MatrixEvent } from "../../src/models/event"; +import { DeviceInfo } from "../../src/crypto/deviceinfo"; + +class MockWidgetApi extends EventEmitter { + public start = jest.fn(); + public requestCapability = jest.fn(); + public requestCapabilities = jest.fn(); + public requestCapabilityToSendState = jest.fn(); + public requestCapabilityToReceiveState = jest.fn(); + public requestCapabilityToSendToDevice = jest.fn(); + public requestCapabilityToReceiveToDevice = jest.fn(); + public sendStateEvent = jest.fn(); + public sendToDevice = jest.fn(); + public readStateEvents = jest.fn(() => []); + public getTurnServers = jest.fn(() => []); +} + +describe("RoomWidgetClient", () => { + let widgetApi: MockedObject; + let client: MatrixClient; + + beforeEach(() => { + widgetApi = new MockWidgetApi() as unknown as MockedObject; + }); + + afterEach(() => { + client.stopClient(); + }); + + const makeClient = async (capabilities: ICapabilities): Promise => { + const baseUrl = "https://example.org"; + client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl }); + widgetApi.emit("ready"); + await client.startClient(); + }; + + describe("state events", () => { + const event = new MatrixEvent({ + type: "org.example.foo", + event_id: "$sfkjfsksdkfsd", + room_id: "!1:example.org", + sender: "@alice:example.org", + state_key: "bar", + content: { hello: "world" }, + }).getEffectiveEvent(); + + it("sends", async () => { + await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); + expect(widgetApi.requestCapabilityToSendState).toHaveBeenCalledWith("org.example.foo", "bar"); + await client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar"); + expect(widgetApi.sendStateEvent).toHaveBeenCalledWith("org.example.foo", "bar", { hello: "world" }); + }); + + it("refuses to send to other rooms", async () => { + await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); + expect(widgetApi.requestCapabilityToSendState).toHaveBeenCalledWith("org.example.foo", "bar"); + await expect(client.sendStateEvent("!2:example.org", "org.example.foo", { hello: "world" }, "bar")) + .rejects.toBeDefined(); + }); + + it("receives", async () => { + await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); + expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar"); + + const emittedEvent = new Promise(resolve => client.once(ClientEvent.Event, resolve)); + const emittedSync = new Promise(resolve => client.once(ClientEvent.Sync, resolve)); + widgetApi.emit( + `action:${WidgetApiToWidgetAction.SendEvent}`, + new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }), + ); + + // The client should've emitted about the received event + expect((await emittedEvent).getEffectiveEvent()).toEqual(event); + expect(await emittedSync).toEqual(SyncState.Syncing); + // It should've also inserted the event into the room object + const room = client.getRoom("!1:example.org"); + expect(room.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event); + }); + + it("backfills", async () => { + widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) => + eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar" + ? [event] + : [], + ); + + await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); + expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar"); + + const room = client.getRoom("!1:example.org"); + expect(room.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event); + }); + }); + + describe("to-device messages", () => { + it("sends unencrypted", async () => { + await makeClient({ sendToDevice: ["org.example.foo"] }); + expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo"); + + const contentMap = { + "@alice:example.org": { "*": { hello: "alice!" } }, + "@bob:example.org": { bobDesktop: { hello: "bob!" } }, + }; + await client.sendToDevice("org.example.foo", contentMap); + expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, contentMap); + }); + + it("sends encrypted", async () => { + await makeClient({ sendToDevice: ["org.example.foo"] }); + expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo"); + + const payload = { type: "org.example.foo", hello: "world" }; + await client.encryptAndSendToDevices( + [ + { userId: "@alice:example.org", deviceInfo: new DeviceInfo("aliceWeb") }, + { userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobDesktop") }, + ], + payload, + ); + expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", true, { + "@alice:example.org": { aliceWeb: payload }, + "@bob:example.org": { bobDesktop: payload }, + }); + }); + + it.todo("receives"); + }); + + it.todo("gets TURN servers"); +}); diff --git a/src/embedded.ts b/src/embedded.ts index 3d3fdc692f3..e526d259653 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -208,6 +208,7 @@ export class RoomWidgetClient extends MatrixClient { private onToDevice = async (ev: CustomEvent) => { ev.preventDefault(); + // TODO: Mark the event as encrypted if it was! this.emit(ClientEvent.ToDeviceEvent, new MatrixEvent(ev.detail.data)); this.setSyncState(SyncState.Syncing); await this.ack(ev); diff --git a/yarn.lock b/yarn.lock index c2553affe81..09cc30f64de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1455,6 +1455,11 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@types/babel-types@*", "@types/babel-types@^7.0.0": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.11.tgz#263b113fa396fac4373188d73225297fb86f19a9" @@ -1556,6 +1561,15 @@ jest-matcher-utils "^28.0.0" pretty-format "^28.0.0" +"@types/jsdom@^16.2.4": + version "16.2.15" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.15.tgz#6c09990ec43b054e49636cba4d11d54367fc90d6" + integrity sha512-nwF87yjBKuX/roqGYerZZM0Nv1pZDMAT5YhOHYeM/72Fic+VEqJh4nyoqoapzJnW3pUlfxPY5FhgsJtM+dRnQQ== + dependencies: + "@types/node" "*" + "@types/parse5" "^6.0.3" + "@types/tough-cookie" "*" + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -1594,6 +1608,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.45.tgz#155b13a33c665ef2b136f7f245fa525da419e810" integrity sha512-3rKg/L5x0rofKuuUt5zlXzOnKyIHXmIu5R8A0TuNDMF2062/AOIDBciFIjToLEJ/9F9DzkHNot+BpNsMI1OLdQ== +"@types/parse5@^6.0.3": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" + integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g== + "@types/prettier@^2.1.5": version "2.6.4" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.4.tgz#ad899dad022bab6b5a9f0a0fe67c2f7a4a8950ed" @@ -1729,6 +1748,11 @@ JSONStream@^1.0.3: jsonparse "^1.2.0" through ">=2.2.7 <3" +abab@^2.0.5, abab@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + ace-builds@^1.4.13: version "1.8.1" resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.8.1.tgz#5d318fa13d7e6ea947f8a50e42c570c573b29529" @@ -1741,6 +1765,14 @@ acorn-globals@^3.0.0: dependencies: acorn "^4.0.4" +acorn-globals@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" + integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== + dependencies: + acorn "^7.1.1" + acorn-walk "^7.1.1" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1755,7 +1787,7 @@ acorn-node@^1.2.0, acorn-node@^1.3.0, acorn-node@^1.5.2, acorn-node@^1.8.2: acorn-walk "^7.0.0" xtend "^4.0.2" -acorn-walk@^7.0.0: +acorn-walk@^7.0.0, acorn-walk@^7.1.1: version "7.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== @@ -1770,7 +1802,7 @@ acorn@^4.0.4, acorn@~4.0.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" integrity sha512-fu2ygVGuMmlzG8ZeRJ0bvR41nsAkxxhbyk8bZ1SS521Z7vmgJFTQQlfz/Mp/nJexGBz+v8sC9bM6+lNgskt4Ug== -acorn@^7.0.0: +acorn@^7.0.0, acorn@^7.1.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -1780,6 +1812,13 @@ acorn@^8.5.0, acorn@^8.7.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -2196,6 +2235,11 @@ browser-pack@^6.0.1: through2 "^2.0.0" umd "^3.0.0" +browser-process-hrtime@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" + integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== + browser-request@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/browser-request/-/browser-request-0.3.3.tgz#9ece5b5aca89a29932242e18bf933def9876cc17" @@ -2597,7 +2641,7 @@ combine-source-map@^0.8.0, combine-source-map@~0.8.0: lodash.memoize "~3.0.3" source-map "~0.5.3" -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2751,6 +2795,23 @@ crypto-browserify@^3.0.0: randombytes "^2.0.0" randomfill "^1.0.3" +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + 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" @@ -2771,11 +2832,27 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-urls@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" + integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== + dependencies: + abab "^2.0.6" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2790,24 +2867,22 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - decamelize@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decimal.js@^10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" + integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== + dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== -deep-is@^0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== @@ -2934,6 +3009,13 @@ domexception@^1.0.1: dependencies: webidl-conversions "^4.0.2" +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -3090,6 +3172,18 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + eslint-config-google@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a" @@ -3223,7 +3317,7 @@ espree@^9.3.2: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.3.0" -esprima@^4.0.0, esprima@~4.0.0: +esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -3378,7 +3472,7 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@^2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== @@ -3497,6 +3591,15 @@ form-data@^2.5.0: combined-stream "^1.0.6" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -3740,6 +3843,13 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -3750,6 +3860,15 @@ htmlescape@^1.1.0: resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" integrity sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg== +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -3764,11 +3883,26 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -3975,6 +4109,11 @@ is-plain-object@^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.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" @@ -4211,6 +4350,20 @@ jest-each@^28.1.3: jest-util "^28.1.3" pretty-format "^28.1.3" +jest-environment-jsdom@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-28.1.3.tgz#2d4e5d61b7f1d94c3bddfbb21f0308ee506c09fb" + integrity sha512-HnlGUmZRdxfCByd3GM2F100DgQOajUBzEitjGqIREcb45kGjZvRrKUdlaF6escXBdcXNl0OBh+1ZrfeZT3GnAg== + dependencies: + "@jest/environment" "^28.1.3" + "@jest/fake-timers" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/jsdom" "^16.2.4" + "@types/node" "*" + jest-mock "^28.1.3" + jest-util "^28.1.3" + jsdom "^19.0.0" + jest-environment-node@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-28.1.3.tgz#7e74fe40eb645b9d56c0c4b70ca4357faa349be5" @@ -4532,6 +4685,39 @@ jsdoc@^3.6.6: taffydb "2.6.2" underscore "~1.13.2" +jsdom@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-19.0.0.tgz#93e67c149fe26816d38a849ea30ac93677e16b6a" + integrity sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A== + dependencies: + abab "^2.0.5" + acorn "^8.5.0" + acorn-globals "^6.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.1" + decimal.js "^10.3.1" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^10.0.0" + ws "^8.2.3" + xml-name-validator "^4.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -4652,6 +4838,14 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -5029,6 +5223,11 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +nwsapi@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.1.tgz#10a9f268fbf4c461249ebcfe38e359aa36e2577c" + integrity sha512-JYOWTeFoS0Z93587vRJgASD5Ut11fYl5NyihP3KrYBvMe1FRRs6RN7m20SA/16GM4P6hTnZjT+UmDOt38UeXNg== + oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -5082,6 +5281,18 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -5213,6 +5424,11 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse5@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + path-browserify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" @@ -5308,6 +5524,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + pretty-format@^28.0.0, pretty-format@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.3.tgz#c9fba8cedf99ce50963a11b27d982a9ae90970d5" @@ -5362,7 +5583,7 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== -psl@^1.1.28: +psl@^1.1.28, psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== @@ -5828,11 +6049,18 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +saxes@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== + dependencies: + xmlchars "^2.2.0" + sdp-transform@^2.14.1: version "2.14.1" resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.14.1.tgz#2bb443583d478dee217df4caa284c46b870d5827" @@ -6156,6 +6384,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + syntax-error@^1.1.1: version "1.4.0" resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c" @@ -6260,6 +6493,15 @@ token-stream@0.0.1: resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-0.0.1.tgz#ceeefc717a76c4316f126d0b9dbaa55d7e7df01a" integrity sha512-nfjOAu/zAWmX9tgwi5NRp7O7zTDUD1miHiB40klUnAh9qnL1iXdgzcz/i5dMaL5jahcBAaSfmNOBBJBLJW8TEg== +tough-cookie@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.1.2" + tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -6275,6 +6517,13 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -6363,6 +6612,13 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -6501,6 +6757,11 @@ universal-user-agent@^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" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + update-browserslist-db@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz#be06a5eedd62f107b7c19eb5bcefb194411abf38" @@ -6617,6 +6878,20 @@ vue2-ace-editor@^0.0.15: dependencies: brace "^0.11.0" +w3c-hr-time@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" + integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== + dependencies: + browser-process-hrtime "^1.0.0" + +w3c-xmlserializer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923" + integrity sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg== + dependencies: + xml-name-validator "^4.0.0" + walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" @@ -6639,6 +6914,39 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-10.0.0.tgz#37264f720b575b4a311bd4094ed8c760caaa05da" + integrity sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -6699,7 +7007,7 @@ with@^5.0.0: acorn "^3.1.0" acorn-globals "^3.0.0" -word-wrap@^1.2.3: +word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== @@ -6731,11 +7039,26 @@ write-file-atomic@^4.0.1: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@^8.2.3: + version "8.8.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" + integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== + +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + xml@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw== +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + xmlcreate@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" From 91aa81479a803389d2112bcf6980235872bf3154 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 6 Aug 2022 01:02:41 -0400 Subject: [PATCH 17/21] Fix tests --- spec/unit/embedded.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index 5226b5f4e9f..162d71e8258 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -44,6 +44,8 @@ class MockWidgetApi extends EventEmitter { public sendToDevice = jest.fn(); public readStateEvents = jest.fn(() => []); public getTurnServers = jest.fn(() => []); + + public transport = { reply: jest.fn() }; } describe("RoomWidgetClient", () => { From e869aa8af094bfb81d84a4b6d5a779d311f1acf9 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 8 Aug 2022 10:06:35 -0400 Subject: [PATCH 18/21] Fix indirect export --- src/matrix.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix.ts b/src/matrix.ts index 2faead9f561..4fbd3077377 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -53,7 +53,7 @@ export * from './@types/requests'; export * from './@types/search'; export * from './models/room-summary'; export * as ContentHelpers from "./content-helpers"; -export { ICryptoCallbacks } from "./crypto"; // used to be located here +export type { ICryptoCallbacks } from "./crypto"; // used to be located here export { createNewMatrixCall } from "./webrtc/call"; export type { MatrixCall } from "./webrtc/call"; export { From 2b4871b25a7ffaabfe528c519cafd4b17b063e96 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 8 Aug 2022 13:04:44 -0400 Subject: [PATCH 19/21] Add more tests --- spec/unit/embedded.spec.ts | 84 ++++++++++++++++++++++++++++++++++++-- src/embedded.ts | 1 + 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index 162d71e8258..876caff4ee0 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -23,10 +23,15 @@ limitations under the License. // eslint-disable-next-line no-restricted-imports import { EventEmitter } from "events"; import { MockedObject } from "jest-mock"; -import { WidgetApi, WidgetApiToWidgetAction } from "matrix-widget-api"; +import { + WidgetApi, + WidgetApiToWidgetAction, + MatrixCapabilities, + ITurnServer, +} from "matrix-widget-api"; import { createRoomWidgetClient } from "../../src/matrix"; -import { MatrixClient, ClientEvent } from "../../src/client"; +import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client"; import { SyncState } from "../../src/sync"; import { ICapabilities } from "../../src/embedded"; import { MatrixEvent } from "../../src/models/event"; @@ -63,6 +68,7 @@ describe("RoomWidgetClient", () => { const makeClient = async (capabilities: ICapabilities): Promise => { const baseUrl = "https://example.org"; client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl }); + expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages widgetApi.emit("ready"); await client.startClient(); }; @@ -156,8 +162,78 @@ describe("RoomWidgetClient", () => { }); }); - it.todo("receives"); + it("receives", async () => { + await makeClient({ receiveToDevice: ["org.example.foo"] }); + expect(widgetApi.requestCapabilityToReceiveToDevice).toHaveBeenCalledWith("org.example.foo"); + + const event = { + type: "org.example.foo", + sender: "@alice:example.org", + encrypted: false, + content: { hello: "world" }, + }; + + const emittedEvent = new Promise(resolve => client.once(ClientEvent.ToDeviceEvent, resolve)); + const emittedSync = new Promise(resolve => client.once(ClientEvent.Sync, resolve)); + widgetApi.emit( + `action:${WidgetApiToWidgetAction.SendToDevice}`, + new CustomEvent(`action:${WidgetApiToWidgetAction.SendToDevice}`, { detail: { data: event } }), + ); + + expect((await emittedEvent).getEffectiveEvent()).toEqual(event); + expect(await emittedSync).toEqual(SyncState.Syncing); + }); }); - it.todo("gets TURN servers"); + it("gets TURN servers", async () => { + const server1: ITurnServer = { + uris: [ + "turn:turn.example.com:3478?transport=udp", + "turn:10.20.30.40:3478?transport=tcp", + "turns:10.20.30.40:443?transport=tcp", + ], + username: "1443779631:@user:example.com", + password: "JlKfBy1QwLrO20385QyAtEyIv0=", + }; + const server2: ITurnServer = { + uris: [ + "turn:turn.example.com:3478?transport=udp", + "turn:10.20.30.40:3478?transport=tcp", + "turns:10.20.30.40:443?transport=tcp", + ], + username: "1448999322:@user:example.com", + password: "hunter2", + }; + const clientServer1: IClientTurnServer = { + urls: server1.uris, + username: server1.username, + credential: server1.password, + }; + const clientServer2: IClientTurnServer = { + urls: server2.uris, + username: server2.username, + credential: server2.password, + }; + + let emitServer2: () => void; + const getServer2 = new Promise(resolve => emitServer2 = () => resolve(server2)); + widgetApi.getTurnServers.mockImplementation(async function* () { + yield server1; + yield await getServer2; + }); + + await makeClient({ turnServers: true }); + expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC3846TurnServers); + + // The first server should've arrived immediately + expect(client.getTurnServers()).toEqual([clientServer1]); + + // Subsequent servers arrive asynchronously and should emit an event + const emittedServer = new Promise(resolve => + client.once(ClientEvent.TurnServers, resolve), + ); + emitServer2(); + expect(await emittedServer).toEqual([clientServer2]); + expect(client.getTurnServers()).toEqual([clientServer2]); + }); }); diff --git a/src/embedded.ts b/src/embedded.ts index e526d259653..2628ef1f762 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -67,6 +67,7 @@ export class RoomWidgetClient extends MatrixClient { super(opts); // Request capabilities for the functionality this client needs to support + // TODO: Check widget API versions before doing any of this! this.capabilities.sendState?.forEach(({ eventType, stateKey }) => this.widgetApi.requestCapabilityToSendState(eventType, stateKey), ); From 43e957efdf5bc540f361af25e8cca7c86e00afa1 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 8 Aug 2022 14:24:32 -0400 Subject: [PATCH 20/21] Resolve TODOs --- spec/unit/embedded.spec.ts | 14 +++++++++++--- src/embedded.ts | 14 +++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index 876caff4ee0..924b2b8c2da 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -162,14 +162,17 @@ describe("RoomWidgetClient", () => { }); }); - it("receives", async () => { + it.each([ + { encrypted: false, title: "unencrypted" }, + { encrypted: true, title: "encrypted" }, + ])("receives $title", async ({ encrypted }) => { await makeClient({ receiveToDevice: ["org.example.foo"] }); expect(widgetApi.requestCapabilityToReceiveToDevice).toHaveBeenCalledWith("org.example.foo"); const event = { type: "org.example.foo", sender: "@alice:example.org", - encrypted: false, + encrypted, content: { hello: "world" }, }; @@ -180,7 +183,12 @@ describe("RoomWidgetClient", () => { new CustomEvent(`action:${WidgetApiToWidgetAction.SendToDevice}`, { detail: { data: event } }), ); - expect((await emittedEvent).getEffectiveEvent()).toEqual(event); + expect((await emittedEvent).getEffectiveEvent()).toEqual({ + type: event.type, + sender: event.sender, + content: event.content, + }); + expect((await emittedEvent).isEncrypted()).toEqual(encrypted); expect(await emittedSync).toEqual(SyncState.Syncing); }); }); diff --git a/src/embedded.ts b/src/embedded.ts index 2628ef1f762..4d22eb771d4 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -25,6 +25,7 @@ import { } from "matrix-widget-api"; import { ISendEventResponse } from "./@types/requests"; +import { EventType } from "./@types/event"; import { logger } from "./logger"; import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts } from "./client"; import { SyncApi, SyncState } from "./sync"; @@ -67,7 +68,6 @@ export class RoomWidgetClient extends MatrixClient { super(opts); // Request capabilities for the functionality this client needs to support - // TODO: Check widget API versions before doing any of this! this.capabilities.sendState?.forEach(({ eventType, stateKey }) => this.widgetApi.requestCapabilityToSendState(eventType, stateKey), ); @@ -209,8 +209,16 @@ export class RoomWidgetClient extends MatrixClient { private onToDevice = async (ev: CustomEvent) => { ev.preventDefault(); - // TODO: Mark the event as encrypted if it was! - this.emit(ClientEvent.ToDeviceEvent, new MatrixEvent(ev.detail.data)); + + const event = new MatrixEvent({ + type: ev.detail.data.type, + sender: ev.detail.data.sender, + content: ev.detail.data.content, + }); + // Mark the event as encrypted if it was, using fake contents and keys since those are unknown to us + if (ev.detail.data.encrypted) event.makeEncrypted(EventType.RoomMessageEncrypted, {}, "", ""); + + this.emit(ClientEvent.ToDeviceEvent, event); this.setSyncState(SyncState.Syncing); await this.ack(ev); }; From 5e87046e3c76c70b4b1782458ff5d59be20e3648 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 9 Aug 2022 09:38:54 -0400 Subject: [PATCH 21/21] Add queueToDevice to RoomWidgetClient --- spec/unit/embedded.spec.ts | 31 ++++++++++++++++++++++++------- src/embedded.ts | 11 +++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index 924b2b8c2da..edac107b976 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -35,6 +35,7 @@ import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../ import { SyncState } from "../../src/sync"; import { ICapabilities } from "../../src/embedded"; import { MatrixEvent } from "../../src/models/event"; +import { ToDeviceBatch } from "../../src/models/ToDeviceMessage"; import { DeviceInfo } from "../../src/crypto/deviceinfo"; class MockWidgetApi extends EventEmitter { @@ -132,19 +133,35 @@ describe("RoomWidgetClient", () => { }); describe("to-device messages", () => { - it("sends unencrypted", async () => { + const unencryptedContentMap = { + "@alice:example.org": { "*": { hello: "alice!" } }, + "@bob:example.org": { bobDesktop: { hello: "bob!" } }, + }; + + it("sends unencrypted (sendToDevice)", async () => { + await makeClient({ sendToDevice: ["org.example.foo"] }); + expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo"); + + await client.sendToDevice("org.example.foo", unencryptedContentMap); + expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap); + }); + + it("sends unencrypted (queueToDevice)", async () => { await makeClient({ sendToDevice: ["org.example.foo"] }); expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo"); - const contentMap = { - "@alice:example.org": { "*": { hello: "alice!" } }, - "@bob:example.org": { bobDesktop: { hello: "bob!" } }, + const batch: ToDeviceBatch = { + eventType: "org.example.foo", + batch: [ + { userId: "@alice:example.org", deviceId: "*", payload: { hello: "alice!" } }, + { userId: "@bob:example.org", deviceId: "bobDesktop", payload: { hello: "bob!" } }, + ], }; - await client.sendToDevice("org.example.foo", contentMap); - expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, contentMap); + await client.queueToDevice(batch); + expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap); }); - it("sends encrypted", async () => { + it("sends encrypted (encryptAndSendToDevices)", async () => { await makeClient({ sendToDevice: ["org.example.foo"] }); expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo"); diff --git a/src/embedded.ts b/src/embedded.ts index 4d22eb771d4..047cd0279c3 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -33,6 +33,7 @@ import { SlidingSyncSdk } from "./sliding-sync-sdk"; import { MatrixEvent } from "./models/event"; import { User } from "./models/user"; import { Room } from "./models/room"; +import { ToDeviceBatch } from "./models/ToDeviceMessage"; import { DeviceInfo } from "./crypto/deviceinfo"; import { IOlmDevice } from "./crypto/algorithms/megolm"; @@ -163,6 +164,16 @@ export class RoomWidgetClient extends MatrixClient { return {}; } + public async queueToDevice({ eventType, batch }: ToDeviceBatch): Promise { + const contentMap: { [userId: string]: { [deviceId: string]: object } } = {}; + for (const { userId, deviceId, payload } of batch) { + if (!contentMap[userId]) contentMap[userId] = {}; + contentMap[userId][deviceId] = payload; + } + + await this.widgetApi.sendToDevice(eventType, false, contentMap); + } + public async encryptAndSendToDevices( userDeviceInfoArr: IOlmDevice[], payload: object,