From 11ce53267b25a3cc87fb32c7db9d3c947c0aec7a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 16 Aug 2023 18:42:51 +0100 Subject: [PATCH 001/113] WIP refactor for removing m.call events --- spec/unit/matrixrtc/CallMembership.spec.ts | 126 +++++++ spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 130 +++++++ .../matrixrtc/MatrixRTCSessionManager.spec.ts | 49 +++ spec/unit/matrixrtc/mocks.ts | 42 +++ src/client.ts | 11 + src/matrixrtc/CallMembership.ts | 95 +++++ src/matrixrtc/MatrixRTCSession.ts | 338 ++++++++++++++++++ src/matrixrtc/MatrixRTCSessonManager.ts | 117 ++++++ src/matrixrtc/focus.ts | 24 ++ src/webrtc/groupCallEventHandler.ts | 1 + 10 files changed, 933 insertions(+) create mode 100644 spec/unit/matrixrtc/CallMembership.spec.ts create mode 100644 spec/unit/matrixrtc/MatrixRTCSession.spec.ts create mode 100644 spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts create mode 100644 spec/unit/matrixrtc/mocks.ts create mode 100644 src/matrixrtc/CallMembership.ts create mode 100644 src/matrixrtc/MatrixRTCSession.ts create mode 100644 src/matrixrtc/MatrixRTCSessonManager.ts create mode 100644 src/matrixrtc/focus.ts diff --git a/spec/unit/matrixrtc/CallMembership.spec.ts b/spec/unit/matrixrtc/CallMembership.spec.ts new file mode 100644 index 00000000000..2c3773fd1b9 --- /dev/null +++ b/spec/unit/matrixrtc/CallMembership.spec.ts @@ -0,0 +1,126 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent } from "../../../src"; +import { CallMembership, CallMembershipData } from "../../../src/matrixrtc/CallMembership"; + +const membershipTemplate: CallMembershipData = { + call_id: "", + scope: "m.room", + application: "m.call", + device_id: "AAAAAAA", + expires: 5000, +}; + +function makeMockEvent(originTs = 0): MatrixEvent { + return { + getTs: jest.fn().mockReturnValue(originTs), + } as unknown as MatrixEvent; +} + +describe("CallMembership", () => { + it("rejects membership with no expiry", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: undefined })); + }).toThrow(); + }); + + it("rejects membership with no device_id", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined })); + }).toThrow(); + }); + + it("rejects membership with no call_id", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined })); + }).toThrow(); + }); + + it("rejects membership with no scope", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined })); + }).toThrow(); + }); + + it("uses event timestamp if no created_ts", () => { + const membership = new CallMembership(makeMockEvent(12345), membershipTemplate); + expect(membership.createdTs()).toEqual(12345); + }); + + it("uses created_ts if present", () => { + const membership = new CallMembership( + makeMockEvent(12345), + Object.assign({}, membershipTemplate, { created_ts: 67890 }), + ); + expect(membership.createdTs()).toEqual(67890); + }); + + it("computes absolute expiry time", () => { + const membership = new CallMembership(makeMockEvent(1000), membershipTemplate); + expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000); + }); + + it("considers memberships unexpired if local age low enough", () => { + const fakeEvent = makeMockEvent(1000); + fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000); + const membership = new CallMembership(fakeEvent, membershipTemplate); + expect(membership.isExpired()).toEqual(false); + }); + + it("considers memberships expired when local age large", () => { + const fakeEvent = makeMockEvent(1000); + fakeEvent.getLocalAge = jest.fn().mockReturnValue(6000); + const membership = new CallMembership(fakeEvent, membershipTemplate); + expect(membership.isExpired()).toEqual(true); + }); + + describe("expiry calculation", () => { + let fakeEvent: MatrixEvent; + let membership: CallMembership; + + beforeEach(() => { + // server origin timestamp for this event is 1000 + fakeEvent = makeMockEvent(1000); + // our clock would have been at 2000 at the creation time (our clock at event receive time - age) + // (ie. the local clock is 1 second ahead of the servers' clocks) + fakeEvent.localTimestamp = 2000; + + // for simplicity's sake, we say that the event's age is zero + fakeEvent.getLocalAge = jest.fn().mockReturnValue(0); + + membership = new CallMembership(fakeEvent!, membershipTemplate); + + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("converts expiry time into local clock", () => { + // for sanity's sake, make sure the server-relative expiry time is what we expect + expect(membership.getAbsoluteExpiry()).toEqual(6000); + // therefore the expiry time converted to our clock should be 1 second later + expect(membership.getLocalExpiry()).toEqual(7000); + }); + + it("calculates time until expiry", () => { + jest.setSystemTime(2000); + expect(membership.getMsUntilExpiry()).toEqual(5000); + }); + }); +}); diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts new file mode 100644 index 00000000000..94fcdfa7a7b --- /dev/null +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -0,0 +1,130 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { EventTimeline, MatrixClient } from "../../../src"; +import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; +import { MatrixRTCSession } from "../../../src/matrixrtc/MatrixRTCSession"; +import { makeMockRoom } from "./mocks"; + +const membershipTemplate: CallMembershipData = { + call_id: "", + scope: "m.room", + application: "m.call", + device_id: "AAAAAAA", + expires: 60 * 60 * 1000, +}; + +describe("MatrixRTCSession", () => { + let client: MatrixClient; + + beforeEach(() => { + client = new MatrixClient({ baseUrl: "base_url" }); + }); + + afterEach(() => { + client.stopClient(); + }); + + it("Creates a room-scoped session from room state", () => { + const mockRoom = makeMockRoom([membershipTemplate]); + + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess?.memberships.length).toEqual(1); + expect(sess?.memberships[0].callId).toEqual(""); + expect(sess?.memberships[0].scope).toEqual("m.room"); + expect(sess?.memberships[0].application).toEqual("m.call"); + expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); + expect(sess?.memberships[0].isExpired()).toEqual(false); + }); + + it("ignores expired memberships events", () => { + const expiredMembership = Object.assign({}, membershipTemplate); + expiredMembership.expires = 1000; + expiredMembership.device_id = "EXPIRED"; + const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]); + // make this event older by adjusting the age param + mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getStateEvents("")[0].getLocalAge = jest + .fn() + .mockReturnValue(10000); + + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess?.memberships.length).toEqual(1); + expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); + }); + + it("honours created_ts", () => { + const expiredMembership = Object.assign({}, membershipTemplate); + expiredMembership.created_ts = 500; + expiredMembership.expires = 1000; + const mockRoom = makeMockRoom([expiredMembership]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500); + }); + + it("returns empty session if no membership events are present", () => { + const mockRoom = makeMockRoom([]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess?.memberships).toHaveLength(0); + }); + + describe("activeRoomSessionForRoom", () => { + it("returns no session if no membership events are present", () => { + const mockRoom = makeMockRoom([]); + const sess = MatrixRTCSession.activeRoomSessionForRoom(client, mockRoom); + expect(sess).toBeUndefined(); + }); + + it("ignores events with no expires_ts", () => { + const expiredMembership = Object.assign({}, membershipTemplate); + (expiredMembership.expires as number | undefined) = undefined; + const mockRoom = makeMockRoom([expiredMembership]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess).toBeUndefined(); + }); + + it("ignores events with no device_id", () => { + const testMembership = Object.assign({}, membershipTemplate); + (testMembership.device_id as string | undefined) = undefined; + const mockRoom = makeMockRoom([testMembership]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess).toBeUndefined(); + }); + + it("ignores events with no call_id", () => { + const testMembership = Object.assign({}, membershipTemplate); + (testMembership.call_id as string | undefined) = undefined; + const mockRoom = makeMockRoom([testMembership]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess).toBeUndefined(); + }); + + it("ignores events with no scope", () => { + const testMembership = Object.assign({}, membershipTemplate); + (testMembership.scope as string | undefined) = undefined; + const mockRoom = makeMockRoom([testMembership]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess).toBeUndefined(); + }); + + it("ignores anything that's not a room-scoped call (for now)", () => { + const testMembership = Object.assign({}, membershipTemplate); + testMembership.scope = "m.user"; + const mockRoom = makeMockRoom([testMembership]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess).toBeUndefined(); + }); + }); +}); diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts new file mode 100644 index 00000000000..fda00137f80 --- /dev/null +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -0,0 +1,49 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClient } from "../../../src"; +import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; +import { makeMockRoom } from "./mocks"; + +const membershipTemplate: CallMembershipData = { + call_id: "", + scope: "m.room", + application: "m.call", + device_id: "AAAAAAA", + expires: 60 * 60 * 1000, +}; + +describe("MatrixRTCSessionManager", () => { + let client: MatrixClient; + + beforeEach(async () => { + client = new MatrixClient({ baseUrl: "base_url" }); + }); + + afterEach(() => { + client.stopClient(); + }); + + it("Gets MatrixRTC sessions accross multiple rooms", () => { + jest.spyOn(client, "getRooms").mockReturnValue([ + makeMockRoom([membershipTemplate]), + makeMockRoom([membershipTemplate]), + ]); + + const sessions = client.matrixRTC.getAllSessions(); + expect(sessions).toHaveLength(2); + }); +}); diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts new file mode 100644 index 00000000000..4a6f21afe66 --- /dev/null +++ b/spec/unit/matrixrtc/mocks.ts @@ -0,0 +1,42 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { EventType, Room } from "../../../src"; +import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; + +export function makeMockRoom(memberships: CallMembershipData[]): Room { + return { + getLiveTimeline: jest.fn().mockReturnValue({ + getState: jest.fn().mockReturnValue(makeMockRoomState(memberships)), + }), + } as unknown as Room; +} + +function makeMockRoomState(memberships: CallMembershipData[]) { + return { + getStateEvents: jest.fn().mockReturnValue([ + { + getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix), + getContent: jest.fn().mockReturnValue({ + memberships: memberships, + }), + getSender: jest.fn().mockReturnValue("@mock:user.example"), + getTs: jest.fn().mockReturnValue(1000), + getLocalAge: jest.fn().mockReturnValue(10), + }, + ]), + }; +} diff --git a/src/client.ts b/src/client.ts index 94f01c173a3..38106956ec4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -220,6 +220,7 @@ import { ServerSideSecretStorageImpl, } from "./secret-storage"; import { RegisterRequest, RegisterResponse } from "./@types/registration"; +import { MatrixRTCSessionManager } from "./matrixrtc/MatrixRTCSessonManager"; export type Store = IStore; @@ -1264,6 +1265,8 @@ export class MatrixClient extends TypedEventEmitter void; + [MatrixRTCSessionEvent.JoinStateChanged]: (isJoined: boolean) => void; +}; + +/** + * A MatrixRTCSession manages the membership & properties of a MatrixRTC session. + * This class doesn't deal with media at all, just membership & properties of a session. + */ +export class MatrixRTCSession extends TypedEventEmitter { + // How many ms after we joined the call, that our membership should expire, or undefined + // if we're not yet joined + private relativeExpiry: number | undefined; + + private memberEventTimeout?: ReturnType; + private expiryTimeout?: ReturnType; + + private activeFoci: Focus[] | undefined; + + public isJoined(): boolean { + return this.relativeExpiry !== undefined; + } + + /** + * Returns all the call memberships for a room, oldest first + */ + public static callMembershipsForRoom(room: Room): CallMembership[] { + const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS); + if (!roomState) { + logger.warn("Couldn't get state for room " + room.roomId); + throw new Error("Could't get state for room " + room.roomId); + } + const callMemberEvents = roomState.getStateEvents(EventType.GroupCallMemberPrefix); + + const callMemberships: CallMembership[] = []; + for (const memberEvent of callMemberEvents) { + const eventMemberships: CallMembershipData[] = memberEvent.getContent()["memberships"]; + if (eventMemberships === undefined) throw new Error("Malformed member event: no memberships section"); + if (!Array.isArray(eventMemberships)) { + throw new Error("Malformed member event: memberships is not an array"); + } + + for (const membershipData of eventMemberships) { + try { + const membership = new CallMembership(memberEvent, membershipData); + + if (membership.callId !== "" || membership.scope !== "m.room") { + // for now, just ignore anything that isn't the a room scope call + logger.info(`Ignoring user-scoped call`); + continue; + } + + if (membership.isExpired()) { + logger.info( + `Ignoring expired device membership ${memberEvent.getSender()}/${membership.deviceId}`, + ); + continue; + } + callMemberships.push(membership); + } catch (e) { + logger.warn("Couldn't construct call membership: ", e); + } + } + } + + callMemberships.sort((a, b) => a.createdTs() - b.createdTs()); + + return callMemberships; + } + + /** + * Return a the MatrixRTC for the room, whether there are currently active members or not + */ + public static roomSessionForRoom(client: MatrixClient, room: Room): MatrixRTCSession { + const callMemberships = MatrixRTCSession.callMembershipsForRoom(room); + + return new MatrixRTCSession(client, room, callMemberships); + } + + /** + * Given the state of a room, find the active MatrixRTC Room-scoped session (if any) any return it + * or return undefined if no members are currently joined to the room's MatrixRTC session + */ + public static activeRoomSessionForRoom(client: MatrixClient, room: Room): MatrixRTCSession | undefined { + const callMemberships = MatrixRTCSession.callMembershipsForRoom(room); + + if (callMemberships.length === 0) return undefined; + + return new MatrixRTCSession(client, room, callMemberships); + } + + private constructor( + private readonly client: MatrixClient, + public readonly room: Room, + public memberships: CallMembership[], + ) { + super(); + } + + /** + * Announces this user and device as joined to the MatrixRTC session, + * and continues to update the membership event to keep it valid until + * leaveRoomSession() is called + * This will not subscribe to updates: remember to call subscribe() separately if + * desired. + */ + public joinRoomSession(activeFoci: Focus[]): void { + if (this.isJoined()) { + logger.info(`Already joined to session in room ${this.room.roomId}: ignoring join call`); + return; + } + + logger.info(`Joining call session in room ${this.room.roomId}`); + this.activeFoci = activeFoci; + this.relativeExpiry = MEMBERSHIP_EXPIRY_TIME; + this.emit(MatrixRTCSessionEvent.JoinStateChanged, true); + this.updateCallMembershipEvent(); + } + + /** + * Announces this user and device as having left the MatrixRTC session + * and stops scheduled updates. + * This will not unsubscribe from updates: remember to call unsubscribe() separately if + * desired. + */ + public leaveRoomSession(): void { + if (!this.isJoined()) { + logger.info(`Not joined to session in room ${this.room.roomId}: ignoring leave call`); + return; + } + + logger.info(`Leaving call session in room ${this.room.roomId}`); + this.relativeExpiry = undefined; + this.activeFoci = undefined; + this.emit(MatrixRTCSessionEvent.JoinStateChanged, false); + this.updateCallMembershipEvent(); + } + + /** + * Sets a timer for the soonest membership expiry + */ + private setExpiryTimer(): void { + if (this.expiryTimeout) { + clearTimeout(this.expiryTimeout); + this.expiryTimeout = undefined; + } + + let soonestExpiry; + for (const membership of this.memberships) { + const thisExpiry = membership.getMsUntilExpiry(); + if (soonestExpiry === undefined || thisExpiry < soonestExpiry) { + soonestExpiry = thisExpiry; + } + } + + if (soonestExpiry != undefined) { + setTimeout(this.onMembershipUpdate, soonestExpiry); + } + } + + public getOldestMembership(): CallMembership | undefined { + return this.memberships[0]; + } + + public onMembershipUpdate = (): void => { + const oldMemberships = this.memberships; + this.memberships = MatrixRTCSession.callMembershipsForRoom(this.room); + + const changed = + oldMemberships.length != this.memberships.length || + oldMemberships.some((m, i) => !CallMembership.equal(m, this.memberships[i])); + + if (changed) { + logger.info(`Memberships for call in room ${this.room.roomId} have changed: emitting`); + this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships); + } + + this.setExpiryTimer(); + }; + + /** + * Constructs our own membership + * @param prevEvent - The previous version of our call membership, if any + */ + private makeMyMembership(prevMembership?: CallMembership): CallMembershipData { + if (this.relativeExpiry === undefined) { + throw new Error("Tried to create our own membership event when we're not joined!"); + } + + const m: CallMembershipData = { + call_id: "", + scope: "m.room", + application: "m.call", + device_id: this.client.getDeviceId()!, + expires: this.relativeExpiry, + foci_active: this.activeFoci, + }; + + if (prevMembership) m.created_ts = prevMembership.createdTs(); + + return m; + } + + private updateCallMembershipEvent = async (): Promise => { + if (this.memberEventTimeout) { + clearTimeout(this.memberEventTimeout); + this.memberEventTimeout = undefined; + } + + const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS); + const localUserId = this.client.getUserId(); + const localDeviceId = this.client.getDeviceId(); + + if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!"); + + if (!roomState) throw new Error("Couldn't get room state for room " + this.room.roomId); + + const myCallMemberEvent = roomState.getStateEvents(EventType.GroupCallMemberPrefix, localUserId); + const content = myCallMemberEvent?.getContent>() ?? {}; + const memberships: CallMembershipData[] = Array.isArray(content["memberships"]) ? content["memberships"] : []; + + const myPrevMembershipData = memberships.find((m) => m.device_id === localDeviceId); + let myPrevMembership; + try { + if (myCallMemberEvent && myPrevMembershipData) { + myPrevMembership = new CallMembership(myCallMemberEvent, myPrevMembershipData); + } + } catch (e) { + // This would indicate a bug or something weird if our own call membership + // wasn't valid + logger.warn("Our previous call membership was invalid - this shouldn't happen.", e); + } + + // work out if we need to update our membership event + let needsUpdate = false; + // Need to update if there's a membership for us but we're not joined (valid or otherwise) + if (!this.isJoined() && myPrevMembershipData) needsUpdate = true; + if (this.isJoined()) { + // ...or if we are joined, but there's no valid membership event + if (!myPrevMembership) { + needsUpdate = true; + } else if (myPrevMembership.getMsUntilExpiry() < MEMBERSHIP_EXPIRY_TIME / 2) { + // ...or if the expiry time needs bumping + needsUpdate = true; + this.relativeExpiry! += MEMBERSHIP_EXPIRY_TIME; + } + } + + if (!needsUpdate) return; + + const filterExpired = (m: CallMembershipData): boolean => { + let membershipObj; + try { + membershipObj = new CallMembership(myCallMemberEvent!, m); + } catch (e) { + return false; + } + + return !membershipObj.isExpired(); + }; + + const transformMemberships = (m: CallMembershipData): CallMembershipData => { + if (m.created_ts === undefined) { + // we need to fill this in with the origin_server_ts from its original event + m.created_ts = myCallMemberEvent!.getTs(); + } + + return m; + }; + + // Filter our any invalid or expired memberships, and also our own - we'll add that back in next + let newMemberships = memberships.filter(filterExpired).filter((m) => m.device_id !== localDeviceId); + + // If we're joined, add our own + if (this.isJoined()) { + newMemberships.push(this.makeMyMembership(myPrevMembership)); + } + + // Fix up any memberships that need their created_ts adding + newMemberships = newMemberships.map(transformMemberships); + + const newContent = { + memberships: newMemberships, + }; + + let resendDelay; + try { + await this.client.sendStateEvent( + this.room.roomId, + EventType.GroupCallMemberPrefix, + newContent, + localUserId, + ); + + // check in 2 mins to see if we need to refresh our member event + if (this.isJoined()) resendDelay = 2 * 60 * 1000; + } catch (e) { + resendDelay = CALL_MEMBER_EVENT_RETRY_DELAY_MIN + Math.random() * 2000; + logger.warn(`Failed to send call member event: retrying in ${resendDelay}`); + } + + if (resendDelay) this.memberEventTimeout = setTimeout(this.updateCallMembershipEvent, resendDelay); + }; +} diff --git a/src/matrixrtc/MatrixRTCSessonManager.ts b/src/matrixrtc/MatrixRTCSessonManager.ts new file mode 100644 index 00000000000..badc0d52682 --- /dev/null +++ b/src/matrixrtc/MatrixRTCSessonManager.ts @@ -0,0 +1,117 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { logger } from "../logger"; +import { ClientEvent, MatrixClient, MatrixEvent, Room, RoomState, RoomStateEvent } from "../matrix"; +import { TypedEventEmitter } from "../models/typed-event-emitter"; +import { MatrixRTCSession } from "./MatrixRTCSession"; + +enum MatrixRTCSessionManagerEvents { + // A member has joined the MatrixRTC session, creating an active session in a room where there wasn't previously + SessionStarted = "session_started", + // All participants have left a given MatrixRTC session. + SessionEnded = "session_ended", +} + +type EventHandlerMap = { + [MatrixRTCSessionManagerEvents.SessionStarted]: (roomId: string, session: MatrixRTCSession) => void; + [MatrixRTCSessionManagerEvents.SessionEnded]: (roomId: string, session: MatrixRTCSession) => void; +}; + +export class MatrixRTCSessionManager extends TypedEventEmitter { + // Room-scoped sessions that have active members + private activeRoomSessions = new Map(); + + public constructor(private client: MatrixClient) { + super(); + } + + public start(): void { + this.client.on(ClientEvent.Room, this.onRoom); + this.client.on(RoomStateEvent.Events, this.onRoomState); + } + + public stop(): void { + this.client.removeListener(ClientEvent.Room, this.onRoom); + this.client.removeListener(RoomStateEvent.Events, this.onRoomState); + } + + /** + * Get a list of all ongoing MatrixRTC sessions the client knows about + * (whether the client is joined to them or not) + */ + public getAllSessions(): MatrixRTCSession[] { + const sessions: MatrixRTCSession[] = []; + + for (const room of this.client.getRooms()) { + const session = this.getRoomSession(room); + if (session) sessions.push(session); + } + + return sessions; + } + + /** + * Gets the main MatrixRTC session for a room, or undefined if there is + * no current session + */ + public getActiveRoomSession(room: Room): MatrixRTCSession | undefined { + return MatrixRTCSession.activeRoomSessionForRoom(this.client, room); + } + + /** + * Gets the main MatrixRTC session for a room, returning an empty session + * if no members are currently participating + */ + public getRoomSession(room: Room): MatrixRTCSession { + return MatrixRTCSession.roomSessionForRoom(this.client, room); + } + + private onRoom = (room: Room): void => { + this.refreshRoom(room); + }; + + private onRoomState = (event: MatrixEvent, _state: RoomState): void => { + const room = this.client.getRoom(event.getRoomId()); + if (!room) { + logger.error(`Got room state event for unknown room ${event.getRoomId()}!`); + return; + } + + this.refreshRoom(room); + }; + + private refreshRoom(room: Room): void { + const sess = this.getActiveRoomSession(room); + if (sess == undefined && this.activeRoomSessions.has(room.roomId)) { + this.emit( + MatrixRTCSessionManagerEvents.SessionEnded, + room.roomId, + this.activeRoomSessions.get(room.roomId)!, + ); + this.activeRoomSessions.delete(room.roomId); + } else if (sess !== undefined && !this.activeRoomSessions.has(room.roomId)) { + this.activeRoomSessions.set(room.roomId, sess); + this.emit( + MatrixRTCSessionManagerEvents.SessionStarted, + room.roomId, + this.activeRoomSessions.get(room.roomId)!, + ); + } else if (sess) { + sess.onMembershipUpdate(); + } + } +} diff --git a/src/matrixrtc/focus.ts b/src/matrixrtc/focus.ts new file mode 100644 index 00000000000..6892bbf15c0 --- /dev/null +++ b/src/matrixrtc/focus.ts @@ -0,0 +1,24 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Information about a MatrixRTC conference focus. The only attribute that + * the js-sdk (currently) knows about is the type: applications can extend + * this class for different types of focus. + */ +export interface Focus { + type: string; +} diff --git a/src/webrtc/groupCallEventHandler.ts b/src/webrtc/groupCallEventHandler.ts index 1500e191862..4463f9d10fe 100644 --- a/src/webrtc/groupCallEventHandler.ts +++ b/src/webrtc/groupCallEventHandler.ts @@ -84,6 +84,7 @@ export class GroupCallEventHandler { } public stop(): void { + this.client.removeListener(ClientEvent.Room, this.onRoomsChanged); this.client.removeListener(RoomStateEvent.Events, this.onRoomStateChanged); } From 38f9b56f3d31a1299ed55ac4c2aa8032c97f1fa5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 09:03:52 +0100 Subject: [PATCH 002/113] Always remember rtcsessions since we need to only have one instance --- src/matrixrtc/MatrixRTCSession.ts | 18 +++++--- src/matrixrtc/MatrixRTCSessonManager.ts | 57 +++++++++++-------------- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 9fad38e040e..b86520de21e 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -69,9 +69,13 @@ export class MatrixRTCSession extends TypedEventEmitter m.device_id !== localDeviceId); + // Fix up any memberships that need their created_ts adding + newMemberships = newMemberships.map(transformMemberships); + // If we're joined, add our own if (this.isJoined()) { newMemberships.push(this.makeMyMembership(myPrevMembership)); } - // Fix up any memberships that need their created_ts adding - newMemberships = newMemberships.map(transformMemberships); - const newContent = { memberships: newMemberships, }; diff --git a/src/matrixrtc/MatrixRTCSessonManager.ts b/src/matrixrtc/MatrixRTCSessonManager.ts index badc0d52682..51a1918e030 100644 --- a/src/matrixrtc/MatrixRTCSessonManager.ts +++ b/src/matrixrtc/MatrixRTCSessonManager.ts @@ -32,8 +32,10 @@ type EventHandlerMap = { }; export class MatrixRTCSessionManager extends TypedEventEmitter { - // Room-scoped sessions that have active members - private activeRoomSessions = new Map(); + // All the room-scoped sessions we know about. This will include any where the app + // has queried for the MatrixRTC sessions in a room, whether it's ever had any members + // or not) + private roomSessions = new Map(); public constructor(private client: MatrixClient) { super(); @@ -50,18 +52,12 @@ export class MatrixRTCSessionManager extends TypedEventEmitter m.memberships.length > 0); } /** @@ -69,7 +65,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter { @@ -95,23 +95,18 @@ export class MatrixRTCSessionManager extends TypedEventEmitter 0; + + sess.onMembershipUpdate(); + + const nowActive = sess.memberships.length > 0; + + if (wasActive && !nowActive) { + this.emit(MatrixRTCSessionManagerEvents.SessionEnded, room.roomId, this.roomSessions.get(room.roomId)!); + } else if (!wasActive && nowActive) { + this.emit(MatrixRTCSessionManagerEvents.SessionStarted, room.roomId, this.roomSessions.get(room.roomId)!); } } } From 426f498ddb6cafad152882bf0601008ff4f24d29 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 14:32:11 +0100 Subject: [PATCH 003/113] Fix tests --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 6 ------ spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts | 11 +++++++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 94fcdfa7a7b..1ae3c91dfa9 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -81,12 +81,6 @@ describe("MatrixRTCSession", () => { }); describe("activeRoomSessionForRoom", () => { - it("returns no session if no membership events are present", () => { - const mockRoom = makeMockRoom([]); - const sess = MatrixRTCSession.activeRoomSessionForRoom(client, mockRoom); - expect(sess).toBeUndefined(); - }); - it("ignores events with no expires_ts", () => { const expiredMembership = Object.assign({}, membershipTemplate); (expiredMembership.expires as number | undefined) = undefined; diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index fda00137f80..bd20e5f1d84 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -37,13 +37,20 @@ describe("MatrixRTCSessionManager", () => { client.stopClient(); }); - it("Gets MatrixRTC sessions accross multiple rooms", () => { + it("Gets active MatrixRTC sessions accross multiple rooms", () => { jest.spyOn(client, "getRooms").mockReturnValue([ makeMockRoom([membershipTemplate]), makeMockRoom([membershipTemplate]), ]); - const sessions = client.matrixRTC.getAllSessions(); + const sessions = client.matrixRTC.getActiveSessions(); expect(sessions).toHaveLength(2); }); + + it("Ignores inactive sessions", () => { + jest.spyOn(client, "getRooms").mockReturnValue([makeMockRoom([membershipTemplate]), makeMockRoom([])]); + + const sessions = client.matrixRTC.getActiveSessions(); + expect(sessions).toHaveLength(1); + }); }); From 542ce1f00efa3d699abf43032c8e9654ba5e0b95 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 14:44:01 +0100 Subject: [PATCH 004/113] Fix import loop --- src/matrixrtc/MatrixRTCSession.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index b86520de21e..aab4c21c3e8 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -15,7 +15,8 @@ limitations under the License. */ import { logger } from "../logger"; -import { EventTimeline, EventType, MatrixClient, Room, TypedEventEmitter } from "../matrix"; +import { EventTimeline, EventType, MatrixClient, Room } from "../matrix"; +import { TypedEventEmitter } from "../models/typed-event-emitter"; import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; From 79ad56636afe29fbd8d1f333d7054a1feb9b0ba6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 15:05:51 +0100 Subject: [PATCH 005/113] Fix more cyclic imports & tests --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 78 +++++++++---------- .../matrixrtc/MatrixRTCSessionManager.spec.ts | 23 ++++-- spec/unit/matrixrtc/mocks.ts | 2 + src/matrixrtc/MatrixRTCSession.ts | 5 +- src/matrixrtc/MatrixRTCSessonManager.ts | 5 +- 5 files changed, 65 insertions(+), 48 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 1ae3c91dfa9..3311e6b8f9d 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -80,45 +80,43 @@ describe("MatrixRTCSession", () => { expect(sess?.memberships).toHaveLength(0); }); - describe("activeRoomSessionForRoom", () => { - it("ignores events with no expires_ts", () => { - const expiredMembership = Object.assign({}, membershipTemplate); - (expiredMembership.expires as number | undefined) = undefined; - const mockRoom = makeMockRoom([expiredMembership]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - expect(sess).toBeUndefined(); - }); - - it("ignores events with no device_id", () => { - const testMembership = Object.assign({}, membershipTemplate); - (testMembership.device_id as string | undefined) = undefined; - const mockRoom = makeMockRoom([testMembership]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - expect(sess).toBeUndefined(); - }); - - it("ignores events with no call_id", () => { - const testMembership = Object.assign({}, membershipTemplate); - (testMembership.call_id as string | undefined) = undefined; - const mockRoom = makeMockRoom([testMembership]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - expect(sess).toBeUndefined(); - }); - - it("ignores events with no scope", () => { - const testMembership = Object.assign({}, membershipTemplate); - (testMembership.scope as string | undefined) = undefined; - const mockRoom = makeMockRoom([testMembership]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - expect(sess).toBeUndefined(); - }); - - it("ignores anything that's not a room-scoped call (for now)", () => { - const testMembership = Object.assign({}, membershipTemplate); - testMembership.scope = "m.user"; - const mockRoom = makeMockRoom([testMembership]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - expect(sess).toBeUndefined(); - }); + it("ignores events with no expires_ts", () => { + const expiredMembership = Object.assign({}, membershipTemplate); + (expiredMembership.expires as number | undefined) = undefined; + const mockRoom = makeMockRoom([expiredMembership]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess.memberships).toHaveLength(0); + }); + + it("ignores events with no device_id", () => { + const testMembership = Object.assign({}, membershipTemplate); + (testMembership.device_id as string | undefined) = undefined; + const mockRoom = makeMockRoom([testMembership]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess.memberships).toHaveLength(0); + }); + + it("ignores events with no call_id", () => { + const testMembership = Object.assign({}, membershipTemplate); + (testMembership.call_id as string | undefined) = undefined; + const mockRoom = makeMockRoom([testMembership]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess.memberships).toHaveLength(0); + }); + + it("ignores events with no scope", () => { + const testMembership = Object.assign({}, membershipTemplate); + (testMembership.scope as string | undefined) = undefined; + const mockRoom = makeMockRoom([testMembership]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess.memberships).toHaveLength(0); + }); + + it("ignores anything that's not a room-scoped call (for now)", () => { + const testMembership = Object.assign({}, membershipTemplate); + testMembership.scope = "m.user"; + const mockRoom = makeMockRoom([testMembership]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess.memberships).toHaveLength(0); }); }); diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index bd20e5f1d84..05d66ad75ae 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "../../../src"; +import { ClientEvent, MatrixClient } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { makeMockRoom } from "./mocks"; @@ -31,24 +31,35 @@ describe("MatrixRTCSessionManager", () => { beforeEach(async () => { client = new MatrixClient({ baseUrl: "base_url" }); + client.matrixRTC.start(); }); afterEach(() => { + client.matrixRTC.stop(); client.stopClient(); }); it("Gets active MatrixRTC sessions accross multiple rooms", () => { - jest.spyOn(client, "getRooms").mockReturnValue([ - makeMockRoom([membershipTemplate]), - makeMockRoom([membershipTemplate]), - ]); + const room1 = makeMockRoom([membershipTemplate]); + const room2 = makeMockRoom([membershipTemplate]); + + jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]); + + client.emit(ClientEvent.Room, room1); + client.emit(ClientEvent.Room, room2); const sessions = client.matrixRTC.getActiveSessions(); expect(sessions).toHaveLength(2); }); it("Ignores inactive sessions", () => { - jest.spyOn(client, "getRooms").mockReturnValue([makeMockRoom([membershipTemplate]), makeMockRoom([])]); + const room1 = makeMockRoom([membershipTemplate]); + const room2 = makeMockRoom([]); + + jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]); + + client.emit(ClientEvent.Room, room1); + client.emit(ClientEvent.Room, room2); const sessions = client.matrixRTC.getActiveSessions(); expect(sessions).toHaveLength(1); diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index 4a6f21afe66..cb66cdb48af 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -16,9 +16,11 @@ limitations under the License. import { EventType, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; +import { randomString } from "../../../src/randomstring"; export function makeMockRoom(memberships: CallMembershipData[]): Room { return { + roomId: randomString(8), getLiveTimeline: jest.fn().mockReturnValue({ getState: jest.fn().mockReturnValue(makeMockRoomState(memberships)), }), diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index aab4c21c3e8..623a32edf3a 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -15,8 +15,11 @@ limitations under the License. */ import { logger } from "../logger"; -import { EventTimeline, EventType, MatrixClient, Room } from "../matrix"; import { TypedEventEmitter } from "../models/typed-event-emitter"; +import { EventTimeline } from "../models/event-timeline"; +import { Room } from "../models/room"; +import { MatrixClient } from "../client"; +import { EventType } from "../@types/event"; import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; diff --git a/src/matrixrtc/MatrixRTCSessonManager.ts b/src/matrixrtc/MatrixRTCSessonManager.ts index 51a1918e030..8035340f9c4 100644 --- a/src/matrixrtc/MatrixRTCSessonManager.ts +++ b/src/matrixrtc/MatrixRTCSessonManager.ts @@ -15,8 +15,11 @@ limitations under the License. */ import { logger } from "../logger"; -import { ClientEvent, MatrixClient, MatrixEvent, Room, RoomState, RoomStateEvent } from "../matrix"; +import { MatrixClient, ClientEvent } from "../client"; import { TypedEventEmitter } from "../models/typed-event-emitter"; +import { Room } from "../models/room"; +import { RoomState, RoomStateEvent } from "../models/room-state"; +import { MatrixEvent } from "../models/event"; import { MatrixRTCSession } from "./MatrixRTCSession"; enum MatrixRTCSessionManagerEvents { From 3d715fc8ed2c4f2a32e98c610559b21b0eab90a1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 16:17:59 +0100 Subject: [PATCH 006/113] Test session joining --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 53 +++++++++++++++++--- spec/unit/matrixrtc/mocks.ts | 19 ++++--- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 3311e6b8f9d..79d27f3a25d 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventTimeline, MatrixClient } from "../../../src"; +import { EventType, MatrixClient } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession } from "../../../src/matrixrtc/MatrixRTCSession"; import { makeMockRoom } from "./mocks"; @@ -27,11 +27,15 @@ const membershipTemplate: CallMembershipData = { expires: 60 * 60 * 1000, }; +const mockFocus = { type: "mock" }; + describe("MatrixRTCSession", () => { let client: MatrixClient; beforeEach(() => { client = new MatrixClient({ baseUrl: "base_url" }); + client.getUserId = jest.fn().mockReturnValue("@alice:example.org"); + client.getDeviceId = jest.fn().mockReturnValue("AAAAAAA"); }); afterEach(() => { @@ -54,11 +58,7 @@ describe("MatrixRTCSession", () => { const expiredMembership = Object.assign({}, membershipTemplate); expiredMembership.expires = 1000; expiredMembership.device_id = "EXPIRED"; - const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]); - // make this event older by adjusting the age param - mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getStateEvents("")[0].getLocalAge = jest - .fn() - .mockReturnValue(10000); + const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], 10000); const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships.length).toEqual(1); @@ -119,4 +119,45 @@ describe("MatrixRTCSession", () => { const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess.memberships).toHaveLength(0); }); + + describe("isJoined", () => { + it("starts un-joined", () => { + const mockRoom = makeMockRoom([]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess.isJoined()).toEqual(false); + }); + + it("shows joined once join is called", () => { + const mockRoom = makeMockRoom([]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess.joinRoomSession([mockFocus]); + expect(sess.isJoined()).toEqual(true); + }); + }); + + it("sends a membership event when joining a call", () => { + client.sendStateEvent = jest.fn(); + + const mockRoom = makeMockRoom([]); + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess.joinRoomSession([mockFocus]); + + expect(client.sendStateEvent).toHaveBeenCalledWith( + mockRoom.roomId, + EventType.GroupCallMemberPrefix, + { + memberships: [ + { + application: "m.call", + scope: "m.room", + call_id: "", + device_id: "AAAAAAA", + expires: 3600000, + foci_active: [{ type: "mock" }], + }, + ], + }, + "@alice:example.org", + ); + }); }); diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index cb66cdb48af..cd8afdcded5 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -18,27 +18,30 @@ import { EventType, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { randomString } from "../../../src/randomstring"; -export function makeMockRoom(memberships: CallMembershipData[]): Room { +export function makeMockRoom(memberships: CallMembershipData[], localAge: number | undefined = undefined): Room { return { roomId: randomString(8), getLiveTimeline: jest.fn().mockReturnValue({ - getState: jest.fn().mockReturnValue(makeMockRoomState(memberships)), + getState: jest.fn().mockReturnValue(makeMockRoomState(memberships, localAge)), }), } as unknown as Room; } -function makeMockRoomState(memberships: CallMembershipData[]) { +function makeMockRoomState(memberships: CallMembershipData[], localAge = 10) { return { - getStateEvents: jest.fn().mockReturnValue([ - { + getStateEvents: (_: string, stateKey: string) => { + const event = { getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix), getContent: jest.fn().mockReturnValue({ memberships: memberships, }), getSender: jest.fn().mockReturnValue("@mock:user.example"), getTs: jest.fn().mockReturnValue(1000), - getLocalAge: jest.fn().mockReturnValue(10), - }, - ]), + getLocalAge: jest.fn().mockReturnValue(localAge), + }; + + if (stateKey !== undefined) return event; + return [event]; + }, }; } From ac6fece957c5cf5fe53606299761aeb521c6845b Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 16:54:51 +0100 Subject: [PATCH 007/113] Attempt to make tests happy --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 2 +- spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 79d27f3a25d..0f878922332 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -39,7 +39,7 @@ describe("MatrixRTCSession", () => { }); afterEach(() => { - client.stopClient(); + client.matrixRTC.stop(); }); it("Creates a room-scoped session from room state", () => { diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index 05d66ad75ae..7f157ea8e95 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -36,7 +36,6 @@ describe("MatrixRTCSessionManager", () => { afterEach(() => { client.matrixRTC.stop(); - client.stopClient(); }); it("Gets active MatrixRTC sessions accross multiple rooms", () => { From 210c3bb1304e561b82b45b5a469beef9d7093ed7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 17:35:36 +0100 Subject: [PATCH 008/113] Always leave calls in the tests to clean up --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 74 +++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 0f878922332..25c77e598ec 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventType, MatrixClient } from "../../../src"; +import { EventType, MatrixClient, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession } from "../../../src/matrixrtc/MatrixRTCSession"; import { makeMockRoom } from "./mocks"; @@ -120,44 +120,50 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - describe("isJoined", () => { + describe("joining", () => { + let sess: MatrixRTCSession; + let mockRoom: Room; + + beforeEach(() => { + mockRoom = makeMockRoom([]); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + }); + + afterEach(() => { + sess!.leaveRoomSession(); + }); + it("starts un-joined", () => { - const mockRoom = makeMockRoom([]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - expect(sess.isJoined()).toEqual(false); + expect(sess!.isJoined()).toEqual(false); }); it("shows joined once join is called", () => { - const mockRoom = makeMockRoom([]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - sess.joinRoomSession([mockFocus]); - expect(sess.isJoined()).toEqual(true); + sess!.joinRoomSession([mockFocus]); + expect(sess!.isJoined()).toEqual(true); }); - }); - - it("sends a membership event when joining a call", () => { - client.sendStateEvent = jest.fn(); - const mockRoom = makeMockRoom([]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); - sess.joinRoomSession([mockFocus]); - - expect(client.sendStateEvent).toHaveBeenCalledWith( - mockRoom.roomId, - EventType.GroupCallMemberPrefix, - { - memberships: [ - { - application: "m.call", - scope: "m.room", - call_id: "", - device_id: "AAAAAAA", - expires: 3600000, - foci_active: [{ type: "mock" }], - }, - ], - }, - "@alice:example.org", - ); + it("sends a membership event when joining a call", () => { + client.sendStateEvent = jest.fn(); + + sess!.joinRoomSession([mockFocus]); + + expect(client.sendStateEvent).toHaveBeenCalledWith( + mockRoom!.roomId, + EventType.GroupCallMemberPrefix, + { + memberships: [ + { + application: "m.call", + scope: "m.room", + call_id: "", + device_id: "AAAAAAA", + expires: 3600000, + foci_active: [{ type: "mock" }], + }, + ], + }, + "@alice:example.org", + ); + }); }); }); From bd1050708578ac9d2077f8dda787bd41d94edcef Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 17:50:49 +0100 Subject: [PATCH 009/113] comment + desperate attempt to work out what's failing --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 25c77e598ec..8f07d11c123 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -130,6 +130,7 @@ describe("MatrixRTCSession", () => { }); afterEach(() => { + // stop the timers sess!.leaveRoomSession(); }); @@ -137,12 +138,12 @@ describe("MatrixRTCSession", () => { expect(sess!.isJoined()).toEqual(false); }); - it("shows joined once join is called", () => { + it.skip("shows joined once join is called", () => { sess!.joinRoomSession([mockFocus]); expect(sess!.isJoined()).toEqual(true); }); - it("sends a membership event when joining a call", () => { + it.skip("sends a membership event when joining a call", () => { client.sendStateEvent = jest.fn(); sess!.joinRoomSession([mockFocus]); From b1dfdeed2edf94e64d55f55d952793587c33df04 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 17:56:19 +0100 Subject: [PATCH 010/113] More test debugging --- spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index 7f157ea8e95..a8ab9d8e473 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -38,7 +38,7 @@ describe("MatrixRTCSessionManager", () => { client.matrixRTC.stop(); }); - it("Gets active MatrixRTC sessions accross multiple rooms", () => { + it.skip("Gets active MatrixRTC sessions accross multiple rooms", () => { const room1 = makeMockRoom([membershipTemplate]); const room2 = makeMockRoom([membershipTemplate]); @@ -51,7 +51,7 @@ describe("MatrixRTCSessionManager", () => { expect(sessions).toHaveLength(2); }); - it("Ignores inactive sessions", () => { + it.skip("Ignores inactive sessions", () => { const room1 = makeMockRoom([membershipTemplate]); const room2 = makeMockRoom([]); From 03868f8ef761405bedc333c0bcaaea80e696780a Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 18:05:22 +0100 Subject: [PATCH 011/113] Okay, so these ones are fine? --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 8f07d11c123..ebadcd452b5 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -138,12 +138,12 @@ describe("MatrixRTCSession", () => { expect(sess!.isJoined()).toEqual(false); }); - it.skip("shows joined once join is called", () => { + it("shows joined once join is called", () => { sess!.joinRoomSession([mockFocus]); expect(sess!.isJoined()).toEqual(true); }); - it.skip("sends a membership event when joining a call", () => { + it("sends a membership event when joining a call", () => { client.sendStateEvent = jest.fn(); sess!.joinRoomSession([mockFocus]); From c9de1cb8b27b5407b47542999c5ed87dc0d9b37f Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 18:22:16 +0100 Subject: [PATCH 012/113] Stop more timers and hopefully have happy tests --- spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts | 4 ++-- src/matrixrtc/MatrixRTCSession.ts | 13 ++++++++++++- src/matrixrtc/MatrixRTCSessonManager.ts | 8 +++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index a8ab9d8e473..7f157ea8e95 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -38,7 +38,7 @@ describe("MatrixRTCSessionManager", () => { client.matrixRTC.stop(); }); - it.skip("Gets active MatrixRTC sessions accross multiple rooms", () => { + it("Gets active MatrixRTC sessions accross multiple rooms", () => { const room1 = makeMockRoom([membershipTemplate]); const room2 = makeMockRoom([membershipTemplate]); @@ -51,7 +51,7 @@ describe("MatrixRTCSessionManager", () => { expect(sessions).toHaveLength(2); }); - it.skip("Ignores inactive sessions", () => { + it("Ignores inactive sessions", () => { const room1 = makeMockRoom([membershipTemplate]); const room2 = makeMockRoom([]); diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 623a32edf3a..723cd6786c0 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -59,6 +59,17 @@ export class MatrixRTCSession extends TypedEventEmitter 0; + const wasActive = sess.memberships.length > 0 && hadSession; sess.onMembershipUpdate(); From 3de7b32462e62e28babb4207b8d24be015b1f17a Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 18:34:36 +0100 Subject: [PATCH 013/113] Test no rejoin --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index ebadcd452b5..50686e825c2 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -166,5 +166,17 @@ describe("MatrixRTCSession", () => { "@alice:example.org", ); }); + + it("does nothing if join called when already joined", () => { + const sendStateEventMock = jest.fn(); + client.sendStateEvent = sendStateEventMock; + + sess!.joinRoomSession([mockFocus]); + + expect(client.sendStateEvent).toHaveBeenCalledTimes(1); + + sess!.joinRoomSession([mockFocus]); + expect(client.sendStateEvent).toHaveBeenCalledTimes(1); + }); }); }); From 73dedc448c13db8445d4e18342baf5583cfdfbdc Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Aug 2023 18:54:48 +0100 Subject: [PATCH 014/113] Test malformed m.call.member events --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 51 ++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 50686e825c2..272f3763688 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -17,6 +17,7 @@ limitations under the License. import { EventType, MatrixClient, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession } from "../../../src/matrixrtc/MatrixRTCSession"; +import { randomString } from "../../../src/randomstring"; import { makeMockRoom } from "./mocks"; const membershipTemplate: CallMembershipData = { @@ -80,7 +81,49 @@ describe("MatrixRTCSession", () => { expect(sess?.memberships).toHaveLength(0); }); - it("ignores events with no expires_ts", () => { + it("safely ignores events with no memberships section", () => { + const mockRoom = { + roomId: randomString(8), + getLiveTimeline: jest.fn().mockReturnValue({ + getState: jest.fn().mockReturnValue({ + getStateEvents: (_type: string, _stateKey: string) => [ + { + getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix), + getContent: jest.fn().mockReturnValue({}), + getSender: jest.fn().mockReturnValue("@mock:user.example"), + getTs: jest.fn().mockReturnValue(1000), + getLocalAge: jest.fn().mockReturnValue(0), + }, + ], + }), + }), + }; + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room); + expect(sess.memberships).toHaveLength(0); + }); + + it("safely ignores events with junk memberships section", () => { + const mockRoom = { + roomId: randomString(8), + getLiveTimeline: jest.fn().mockReturnValue({ + getState: jest.fn().mockReturnValue({ + getStateEvents: (_type: string, _stateKey: string) => [ + { + getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix), + getContent: jest.fn().mockReturnValue({ memberships: "i am a fish" }), + getSender: jest.fn().mockReturnValue("@mock:user.example"), + getTs: jest.fn().mockReturnValue(1000), + getLocalAge: jest.fn().mockReturnValue(0), + }, + ], + }), + }), + }; + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room); + expect(sess.memberships).toHaveLength(0); + }); + + it("ignores memberships with no expires_ts", () => { const expiredMembership = Object.assign({}, membershipTemplate); (expiredMembership.expires as number | undefined) = undefined; const mockRoom = makeMockRoom([expiredMembership]); @@ -88,7 +131,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it("ignores events with no device_id", () => { + it("ignores memberships with no device_id", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.device_id as string | undefined) = undefined; const mockRoom = makeMockRoom([testMembership]); @@ -96,7 +139,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it("ignores events with no call_id", () => { + it("ignores memberships with no call_id", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.call_id as string | undefined) = undefined; const mockRoom = makeMockRoom([testMembership]); @@ -104,7 +147,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it("ignores events with no scope", () => { + it("ignores memberships with no scope", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.scope as string | undefined) = undefined; const mockRoom = makeMockRoom([testMembership]); From 6aeeb7c685caea8646c714b6b2a132ddfb4e19c9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 21 Aug 2023 16:33:08 +0100 Subject: [PATCH 015/113] Test event emitting and also move some code to a more sensible place in the file --- .../matrixrtc/MatrixRTCSessionManager.spec.ts | 41 ++++++++++++++++++- spec/unit/matrixrtc/mocks.ts | 8 ++-- src/matrixrtc/MatrixRTCSession.ts | 30 +++++++------- src/matrixrtc/MatrixRTCSessonManager.ts | 2 +- 4 files changed, 61 insertions(+), 20 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index 7f157ea8e95..4e46c21daad 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -14,8 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ClientEvent, MatrixClient } from "../../../src"; +import { ClientEvent, EventTimeline, MatrixClient } from "../../../src"; +import { RoomStateEvent } from "../../../src/models/room-state"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; +import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessonManager"; import { makeMockRoom } from "./mocks"; const membershipTemplate: CallMembershipData = { @@ -63,4 +65,41 @@ describe("MatrixRTCSessionManager", () => { const sessions = client.matrixRTC.getActiveSessions(); expect(sessions).toHaveLength(1); }); + + it("Fires event when session starts", () => { + const onStarted = jest.fn(); + client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted); + + try { + const room1 = makeMockRoom([membershipTemplate]); + jest.spyOn(client, "getRooms").mockReturnValue([room1]); + + client.emit(ClientEvent.Room, room1); + expect(onStarted).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1)); + } finally { + client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted); + } + }); + + it("Fires event when session ends", () => { + const onEnded = jest.fn(); + client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded); + + const memberships = [membershipTemplate]; + + const room1 = makeMockRoom(memberships); + jest.spyOn(client, "getRooms").mockReturnValue([room1]); + jest.spyOn(client, "getRoom").mockReturnValue(room1); + + client.emit(ClientEvent.Room, room1); + + memberships.splice(0, 1); + + const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!; + const membEvent = roomState.getStateEvents("")[0]; + + client.emit(RoomStateEvent.Events, membEvent, roomState, null); + + expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1)); + }); }); diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index cd8afdcded5..bd37083c4eb 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -19,15 +19,16 @@ import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { randomString } from "../../../src/randomstring"; export function makeMockRoom(memberships: CallMembershipData[], localAge: number | undefined = undefined): Room { + const roomId = randomString(8); return { - roomId: randomString(8), + roomId: roomId, getLiveTimeline: jest.fn().mockReturnValue({ - getState: jest.fn().mockReturnValue(makeMockRoomState(memberships, localAge)), + getState: jest.fn().mockReturnValue(makeMockRoomState(memberships, roomId, localAge)), }), } as unknown as Room; } -function makeMockRoomState(memberships: CallMembershipData[], localAge = 10) { +function makeMockRoomState(memberships: CallMembershipData[], roomId: string, localAge = 10) { return { getStateEvents: (_: string, stateKey: string) => { const event = { @@ -38,6 +39,7 @@ function makeMockRoomState(memberships: CallMembershipData[], localAge = 10) { getSender: jest.fn().mockReturnValue("@mock:user.example"), getTs: jest.fn().mockReturnValue(1000), getLocalAge: jest.fn().mockReturnValue(localAge), + getRoomId: jest.fn().mockReturnValue(roomId), }; if (stateKey !== undefined) return event; diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 723cd6786c0..1c28c56c504 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -55,21 +55,6 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 21 Aug 2023 16:38:12 +0100 Subject: [PATCH 016/113] Test getActiveFoci() --- spec/unit/matrixrtc/CallMembership.spec.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/unit/matrixrtc/CallMembership.spec.ts b/spec/unit/matrixrtc/CallMembership.spec.ts index 2c3773fd1b9..745dad3c9df 100644 --- a/spec/unit/matrixrtc/CallMembership.spec.ts +++ b/spec/unit/matrixrtc/CallMembership.spec.ts @@ -88,6 +88,16 @@ describe("CallMembership", () => { expect(membership.isExpired()).toEqual(true); }); + it("returns active foci", () => { + const fakeEvent = makeMockEvent(); + const mockFocus = { type: "this_is_a_mock_focus" }; + const membership = new CallMembership( + fakeEvent, + Object.assign({}, membershipTemplate, { foci_active: [mockFocus] }), + ); + expect(membership.getActiveFoci()).toEqual([mockFocus]); + }); + describe("expiry calculation", () => { let fakeEvent: MatrixEvent; let membership: CallMembership; From 88d85b40516243be469b7530587c9bc58af10af8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 21 Aug 2023 17:24:48 +0100 Subject: [PATCH 017/113] Test event emitting (and also fix it) --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 28 ++++++++++++++++++-- spec/unit/matrixrtc/mocks.ts | 14 +++++++--- src/matrixrtc/MatrixRTCSession.ts | 1 + 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 272f3763688..c2ca4493285 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import { EventType, MatrixClient, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; -import { MatrixRTCSession } from "../../../src/matrixrtc/MatrixRTCSession"; +import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { randomString } from "../../../src/randomstring"; import { makeMockRoom } from "./mocks"; @@ -59,7 +59,7 @@ describe("MatrixRTCSession", () => { const expiredMembership = Object.assign({}, membershipTemplate); expiredMembership.expires = 1000; expiredMembership.device_id = "EXPIRED"; - const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], 10000); + const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], () => 10000); const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships.length).toEqual(1); @@ -222,4 +222,28 @@ describe("MatrixRTCSession", () => { expect(client.sendStateEvent).toHaveBeenCalledTimes(1); }); }); + + it("emits an event at the time a membership event expires", () => { + jest.useFakeTimers(); + try { + let eventAge = 0; + + const membership = Object.assign({}, membershipTemplate); + const mockRoom = makeMockRoom([membership], () => eventAge); + + const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + const membershipObject = sess.memberships[0]; + + const onMembershipsChanged = jest.fn(); + sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged); + + eventAge = 61 * 1000 * 1000; + jest.advanceTimersByTime(61 * 1000 * 1000); + + expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []); + expect(sess?.memberships.length).toEqual(0); + } finally { + jest.useRealTimers(); + } + }); }); diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index bd37083c4eb..928ec1bb119 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -18,17 +18,22 @@ import { EventType, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { randomString } from "../../../src/randomstring"; -export function makeMockRoom(memberships: CallMembershipData[], localAge: number | undefined = undefined): Room { +export function makeMockRoom( + memberships: CallMembershipData[], + getLocalAge: (() => number) | undefined = undefined, +): Room { const roomId = randomString(8); return { roomId: roomId, getLiveTimeline: jest.fn().mockReturnValue({ - getState: jest.fn().mockReturnValue(makeMockRoomState(memberships, roomId, localAge)), + getState: jest.fn().mockReturnValue(makeMockRoomState(memberships, roomId, getLocalAge)), }), } as unknown as Room; } -function makeMockRoomState(memberships: CallMembershipData[], roomId: string, localAge = 10) { +function makeMockRoomState(memberships: CallMembershipData[], roomId: string, getLocalAge: (() => number) | undefined) { + const getLocalAgeFn = getLocalAge ?? (() => 10); + return { getStateEvents: (_: string, stateKey: string) => { const event = { @@ -38,7 +43,8 @@ function makeMockRoomState(memberships: CallMembershipData[], roomId: string, lo }), getSender: jest.fn().mockReturnValue("@mock:user.example"), getTs: jest.fn().mockReturnValue(1000), - getLocalAge: jest.fn().mockReturnValue(localAge), + getLocalAge: getLocalAgeFn, + localTimestamp: Date.now(), getRoomId: jest.fn().mockReturnValue(roomId), }; diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 1c28c56c504..47cef58fb47 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -133,6 +133,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Tue, 22 Aug 2023 18:05:08 +0100 Subject: [PATCH 018/113] Test membership updating & pruning on join --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 108 ++++++++++++++++--- 1 file changed, 96 insertions(+), 12 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index c2ca4493285..e72b52c6355 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -32,6 +32,7 @@ const mockFocus = { type: "mock" }; describe("MatrixRTCSession", () => { let client: MatrixClient; + let sess: MatrixRTCSession | undefined; beforeEach(() => { client = new MatrixClient({ baseUrl: "base_url" }); @@ -41,12 +42,14 @@ describe("MatrixRTCSession", () => { afterEach(() => { client.matrixRTC.stop(); + if (sess) sess.stop(); + sess = undefined; }); it("Creates a room-scoped session from room state", () => { const mockRoom = makeMockRoom([membershipTemplate]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships.length).toEqual(1); expect(sess?.memberships[0].callId).toEqual(""); expect(sess?.memberships[0].scope).toEqual("m.room"); @@ -61,7 +64,7 @@ describe("MatrixRTCSession", () => { expiredMembership.device_id = "EXPIRED"; const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], () => 10000); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships.length).toEqual(1); expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); }); @@ -71,13 +74,13 @@ describe("MatrixRTCSession", () => { expiredMembership.created_ts = 500; expiredMembership.expires = 1000; const mockRoom = makeMockRoom([expiredMembership]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500); }); it("returns empty session if no membership events are present", () => { const mockRoom = makeMockRoom([]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships).toHaveLength(0); }); @@ -98,7 +101,7 @@ describe("MatrixRTCSession", () => { }), }), }; - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room); expect(sess.memberships).toHaveLength(0); }); @@ -119,7 +122,7 @@ describe("MatrixRTCSession", () => { }), }), }; - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room); expect(sess.memberships).toHaveLength(0); }); @@ -127,7 +130,7 @@ describe("MatrixRTCSession", () => { const expiredMembership = Object.assign({}, membershipTemplate); (expiredMembership.expires as number | undefined) = undefined; const mockRoom = makeMockRoom([expiredMembership]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess.memberships).toHaveLength(0); }); @@ -143,7 +146,7 @@ describe("MatrixRTCSession", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.call_id as string | undefined) = undefined; const mockRoom = makeMockRoom([testMembership]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess.memberships).toHaveLength(0); }); @@ -151,7 +154,7 @@ describe("MatrixRTCSession", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.scope as string | undefined) = undefined; const mockRoom = makeMockRoom([testMembership]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess.memberships).toHaveLength(0); }); @@ -159,12 +162,11 @@ describe("MatrixRTCSession", () => { const testMembership = Object.assign({}, membershipTemplate); testMembership.scope = "m.user"; const mockRoom = makeMockRoom([testMembership]); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess.memberships).toHaveLength(0); }); describe("joining", () => { - let sess: MatrixRTCSession; let mockRoom: Room; beforeEach(() => { @@ -231,7 +233,7 @@ describe("MatrixRTCSession", () => { const membership = Object.assign({}, membershipTemplate); const mockRoom = makeMockRoom([membership], () => eventAge); - const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); const membershipObject = sess.memberships[0]; const onMembershipsChanged = jest.fn(); @@ -246,4 +248,86 @@ describe("MatrixRTCSession", () => { jest.useRealTimers(); } }); + + it("prunes expired memberships on update", () => { + client.sendStateEvent = jest.fn(); + + let eventAge = 0; + + const mockRoom = makeMockRoom( + [ + Object.assign({}, membershipTemplate, { + device_id: "OTHERDEVICE", + expires: 1000, + }), + ], + () => eventAge, + ); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + + // sanity check + expect(sess.memberships).toHaveLength(1); + expect(sess.memberships[0].deviceId).toEqual("OTHERDEVICE"); + + eventAge = 10000; + + sess.joinRoomSession([mockFocus]); + + expect(client.sendStateEvent).toHaveBeenCalledWith( + mockRoom!.roomId, + EventType.GroupCallMemberPrefix, + { + memberships: [ + { + application: "m.call", + scope: "m.room", + call_id: "", + device_id: "AAAAAAA", + expires: 3600000, + foci_active: [mockFocus], + }, + ], + }, + "@alice:example.org", + ); + }); + + it("fills in created_ts for other memberships on update", () => { + client.sendStateEvent = jest.fn(); + + const mockRoom = makeMockRoom([ + Object.assign({}, membershipTemplate, { + device_id: "OTHERDEVICE", + }), + ]); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + + sess.joinRoomSession([mockFocus]); + + expect(client.sendStateEvent).toHaveBeenCalledWith( + mockRoom!.roomId, + EventType.GroupCallMemberPrefix, + { + memberships: [ + { + application: "m.call", + scope: "m.room", + call_id: "", + device_id: "OTHERDEVICE", + expires: 3600000, + created_ts: 1000, + }, + { + application: "m.call", + scope: "m.room", + call_id: "", + device_id: "AAAAAAA", + expires: 3600000, + foci_active: [mockFocus], + }, + ], + }, + "@alice:example.org", + ); + }); }); From 86a25b595a97a09a5f225a00c00b995b84dfa5b2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 22 Aug 2023 18:12:42 +0100 Subject: [PATCH 019/113] Test getOldestMembership() --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index e72b52c6355..161aeceec56 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -166,6 +166,19 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); + describe("getOldestMembership", () => { + it("returns the oldest membership event", () => { + const mockRoom = makeMockRoom([ + Object.assign({}, membershipTemplate, { device_id: "foo", created_ts: 3000 }), + Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }), + Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }), + ]); + + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + expect(sess.getOldestMembership()!.deviceId).toEqual("old"); + }); + }); + describe("joining", () => { let mockRoom: Room; From f0a37cb61cea8ca1486e3533a6703e5538e0b87f Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 22 Aug 2023 20:59:24 +0100 Subject: [PATCH 020/113] Test member event renewal --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 54 +++++++++++++++++++- spec/unit/matrixrtc/mocks.ts | 36 ++++++++----- 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 161aeceec56..c7a37709550 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventType, MatrixClient, Room } from "../../../src"; +import { EventTimeline, EventType, MatrixClient, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { randomString } from "../../../src/randomstring"; -import { makeMockRoom } from "./mocks"; +import { makeMockRoom, mockRTCEvent } from "./mocks"; const membershipTemplate: CallMembershipData = { call_id: "", @@ -236,6 +236,56 @@ describe("MatrixRTCSession", () => { sess!.joinRoomSession([mockFocus]); expect(client.sendStateEvent).toHaveBeenCalledTimes(1); }); + + it("renews membership event before expiry time", async () => { + jest.useFakeTimers(); + let resolveFn: ((_roomId: string, _type: string, val: Record) => void) | undefined; + const eventSentPromise = new Promise>((r) => { + resolveFn = (_roomId: string, _type: string, val: Record) => { + r(val); + }; + }); + try { + const sendStateEventMock = jest.fn().mockImplementation(resolveFn); + client.sendStateEvent = sendStateEventMock; + + sess!.joinRoomSession([mockFocus]); + + const eventContent = await eventSentPromise; + + // definitely should have renewed by 1 second before the expiry! + const timeElapsed = 60 * 60 * 1000 - 1000; + mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getStateEvents = jest + .fn() + .mockReturnValue(mockRTCEvent(eventContent.memberships, mockRoom.roomId, () => timeElapsed)); + + sendStateEventMock.mockClear(); + + jest.setSystemTime(Date.now() + timeElapsed); + jest.advanceTimersByTime(timeElapsed); + + expect(sendStateEventMock).toHaveBeenCalledWith( + mockRoom.roomId, + EventType.GroupCallMemberPrefix, + { + memberships: [ + { + application: "m.call", + scope: "m.room", + call_id: "", + device_id: "AAAAAAA", + expires: 3600000 * 2, + foci_active: [{ type: "mock" }], + created_ts: 1000, + }, + ], + }, + "@alice:example.org", + ); + } finally { + jest.useRealTimers(); + } + }); }); it("emits an event at the time a membership event expires", () => { diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index 928ec1bb119..e791c75861c 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventType, Room } from "../../../src"; +import { EventType, MatrixEvent, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { randomString } from "../../../src/randomstring"; @@ -32,24 +32,32 @@ export function makeMockRoom( } function makeMockRoomState(memberships: CallMembershipData[], roomId: string, getLocalAge: (() => number) | undefined) { - const getLocalAgeFn = getLocalAge ?? (() => 10); - return { getStateEvents: (_: string, stateKey: string) => { - const event = { - getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix), - getContent: jest.fn().mockReturnValue({ - memberships: memberships, - }), - getSender: jest.fn().mockReturnValue("@mock:user.example"), - getTs: jest.fn().mockReturnValue(1000), - getLocalAge: getLocalAgeFn, - localTimestamp: Date.now(), - getRoomId: jest.fn().mockReturnValue(roomId), - }; + const event = mockRTCEvent(memberships, roomId, getLocalAge); if (stateKey !== undefined) return event; return [event]; }, }; } + +export function mockRTCEvent( + memberships: CallMembershipData[], + roomId: string, + getLocalAge: (() => number) | undefined, +): MatrixEvent { + const getLocalAgeFn = getLocalAge ?? (() => 10); + + return { + getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix), + getContent: jest.fn().mockReturnValue({ + memberships: memberships, + }), + getSender: jest.fn().mockReturnValue("@mock:user.example"), + getTs: jest.fn().mockReturnValue(1000), + getLocalAge: getLocalAgeFn, + localTimestamp: Date.now(), + getRoomId: jest.fn().mockReturnValue(roomId), + } as unknown as MatrixEvent; +} From c8ae665eb07079d9f0d568aa1316e792062b5439 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 13:59:01 +0100 Subject: [PATCH 021/113] Don't start the rtc manager until the client has synced Then we can initialise from the state once it's completed. --- src/client.ts | 24 ++++++++++++++---------- src/matrixrtc/MatrixRTCSession.ts | 12 ------------ src/matrixrtc/MatrixRTCSessonManager.ts | 7 +++++++ 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/client.ts b/src/client.ts index 6cb1e7ff1f4..be064ca130f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1340,12 +1340,13 @@ export class MatrixClient extends TypedEventEmitter { + private startCallEventHandlers = (): void => { if (this.isInitialSyncComplete()) { - this.callEventHandler!.start(); - this.groupCallEventHandler!.start(); - this.off(ClientEvent.Sync, this.startCallEventHandler); + if (supportsMatrixCall()) { + this.callEventHandler!.start(); + this.groupCallEventHandler!.start(); + } + + this.matrixRTC.start(); + + this.off(ClientEvent.Sync, this.startCallEventHandlers); } }; diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 47cef58fb47..78c1d0be5c3 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -115,18 +115,6 @@ export class MatrixRTCSession extends TypedEventEmitter 0) { + this.roomSessions.set(room.roomId, session); + } + } + this.client.on(ClientEvent.Room, this.onRoom); this.client.on(RoomStateEvent.Events, this.onRoomState); } From 770d16e1a17c19cd1037df8ac512855a22c4f482 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 14:04:35 +0100 Subject: [PATCH 022/113] Fix type --- src/matrixrtc/MatrixRTCSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 78c1d0be5c3..64cb34e62e9 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -35,8 +35,8 @@ export enum MatrixRTCSessionEvent { export type MatrixRTCSessionEventHandlerMap = { [MatrixRTCSessionEvent.MembershipsChanged]: ( - newMemberships: CallMembership[], oldMemberships: CallMembership[], + newMemberships: CallMembership[], ) => void; [MatrixRTCSessionEvent.JoinStateChanged]: (isJoined: boolean) => void; }; From 5be2d7c440c2d8ffc08c219fdc25925761de9015 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 14:59:03 +0100 Subject: [PATCH 023/113] Remove listeners added in constructor --- src/client.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client.ts b/src/client.ts index be064ca130f..8c4533dce73 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1551,6 +1551,10 @@ export class MatrixClient extends TypedEventEmitter Date: Wed, 23 Aug 2023 15:09:49 +0100 Subject: [PATCH 024/113] Stop the client here too --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index c7a37709550..158dd5207c4 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -41,6 +41,7 @@ describe("MatrixRTCSession", () => { }); afterEach(() => { + client.stopClient(); client.matrixRTC.stop(); if (sess) sess.stop(); sess = undefined; From 2e1aaa8de909399283ed6815fd34171cb9ed7174 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 15:21:44 +0100 Subject: [PATCH 025/113] Stop the client here also also --- spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index 4e46c21daad..a1be7d414dd 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -37,6 +37,7 @@ describe("MatrixRTCSessionManager", () => { }); afterEach(() => { + client.stopClient(); client.matrixRTC.stop(); }); From bcff0391e6c4ca69a0c515caac6fbe5863f1cc3d Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 15:38:47 +0100 Subject: [PATCH 026/113] ARGH. Disable tests to work out which one is causing the exception --- spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index a1be7d414dd..6322d91c233 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -41,7 +41,7 @@ describe("MatrixRTCSessionManager", () => { client.matrixRTC.stop(); }); - it("Gets active MatrixRTC sessions accross multiple rooms", () => { + it.skip("Gets active MatrixRTC sessions accross multiple rooms", () => { const room1 = makeMockRoom([membershipTemplate]); const room2 = makeMockRoom([membershipTemplate]); @@ -54,7 +54,7 @@ describe("MatrixRTCSessionManager", () => { expect(sessions).toHaveLength(2); }); - it("Ignores inactive sessions", () => { + it.skip("Ignores inactive sessions", () => { const room1 = makeMockRoom([membershipTemplate]); const room2 = makeMockRoom([]); @@ -67,7 +67,7 @@ describe("MatrixRTCSessionManager", () => { expect(sessions).toHaveLength(1); }); - it("Fires event when session starts", () => { + it.skip("Fires event when session starts", () => { const onStarted = jest.fn(); client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted); @@ -82,7 +82,7 @@ describe("MatrixRTCSessionManager", () => { } }); - it("Fires event when session ends", () => { + it.skip("Fires event when session ends", () => { const onEnded = jest.fn(); client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded); From 2886455c43bc47d3d3dc6ce7899fdaa6fe9ca9be Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 15:45:04 +0100 Subject: [PATCH 027/113] Disable everything --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 40 ++++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 158dd5207c4..8d5e0690401 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -47,7 +47,7 @@ describe("MatrixRTCSession", () => { sess = undefined; }); - it("Creates a room-scoped session from room state", () => { + it.skip("Creates a room-scoped session from room state", () => { const mockRoom = makeMockRoom([membershipTemplate]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); @@ -59,7 +59,7 @@ describe("MatrixRTCSession", () => { expect(sess?.memberships[0].isExpired()).toEqual(false); }); - it("ignores expired memberships events", () => { + it.skip("ignores expired memberships events", () => { const expiredMembership = Object.assign({}, membershipTemplate); expiredMembership.expires = 1000; expiredMembership.device_id = "EXPIRED"; @@ -70,7 +70,7 @@ describe("MatrixRTCSession", () => { expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); }); - it("honours created_ts", () => { + it.skip("honours created_ts", () => { const expiredMembership = Object.assign({}, membershipTemplate); expiredMembership.created_ts = 500; expiredMembership.expires = 1000; @@ -79,13 +79,13 @@ describe("MatrixRTCSession", () => { expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500); }); - it("returns empty session if no membership events are present", () => { + it.skip("returns empty session if no membership events are present", () => { const mockRoom = makeMockRoom([]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships).toHaveLength(0); }); - it("safely ignores events with no memberships section", () => { + it.skip("safely ignores events with no memberships section", () => { const mockRoom = { roomId: randomString(8), getLiveTimeline: jest.fn().mockReturnValue({ @@ -106,7 +106,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it("safely ignores events with junk memberships section", () => { + it.skip("safely ignores events with junk memberships section", () => { const mockRoom = { roomId: randomString(8), getLiveTimeline: jest.fn().mockReturnValue({ @@ -127,7 +127,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it("ignores memberships with no expires_ts", () => { + it.skip("ignores memberships with no expires_ts", () => { const expiredMembership = Object.assign({}, membershipTemplate); (expiredMembership.expires as number | undefined) = undefined; const mockRoom = makeMockRoom([expiredMembership]); @@ -135,7 +135,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it("ignores memberships with no device_id", () => { + it.skip("ignores memberships with no device_id", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.device_id as string | undefined) = undefined; const mockRoom = makeMockRoom([testMembership]); @@ -143,7 +143,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it("ignores memberships with no call_id", () => { + it.skip("ignores memberships with no call_id", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.call_id as string | undefined) = undefined; const mockRoom = makeMockRoom([testMembership]); @@ -151,7 +151,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it("ignores memberships with no scope", () => { + it.skip("ignores memberships with no scope", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.scope as string | undefined) = undefined; const mockRoom = makeMockRoom([testMembership]); @@ -159,7 +159,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it("ignores anything that's not a room-scoped call (for now)", () => { + it.skip("ignores anything that's not a room-scoped call (for now)", () => { const testMembership = Object.assign({}, membershipTemplate); testMembership.scope = "m.user"; const mockRoom = makeMockRoom([testMembership]); @@ -168,7 +168,7 @@ describe("MatrixRTCSession", () => { }); describe("getOldestMembership", () => { - it("returns the oldest membership event", () => { + it.skip("returns the oldest membership event", () => { const mockRoom = makeMockRoom([ Object.assign({}, membershipTemplate, { device_id: "foo", created_ts: 3000 }), Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }), @@ -193,16 +193,16 @@ describe("MatrixRTCSession", () => { sess!.leaveRoomSession(); }); - it("starts un-joined", () => { + it.skip("starts un-joined", () => { expect(sess!.isJoined()).toEqual(false); }); - it("shows joined once join is called", () => { + it.skip("shows joined once join is called", () => { sess!.joinRoomSession([mockFocus]); expect(sess!.isJoined()).toEqual(true); }); - it("sends a membership event when joining a call", () => { + it.skip("sends a membership event when joining a call", () => { client.sendStateEvent = jest.fn(); sess!.joinRoomSession([mockFocus]); @@ -226,7 +226,7 @@ describe("MatrixRTCSession", () => { ); }); - it("does nothing if join called when already joined", () => { + it.skip("does nothing if join called when already joined", () => { const sendStateEventMock = jest.fn(); client.sendStateEvent = sendStateEventMock; @@ -238,7 +238,7 @@ describe("MatrixRTCSession", () => { expect(client.sendStateEvent).toHaveBeenCalledTimes(1); }); - it("renews membership event before expiry time", async () => { + it.skip("renews membership event before expiry time", async () => { jest.useFakeTimers(); let resolveFn: ((_roomId: string, _type: string, val: Record) => void) | undefined; const eventSentPromise = new Promise>((r) => { @@ -289,7 +289,7 @@ describe("MatrixRTCSession", () => { }); }); - it("emits an event at the time a membership event expires", () => { + it.skip("emits an event at the time a membership event expires", () => { jest.useFakeTimers(); try { let eventAge = 0; @@ -313,7 +313,7 @@ describe("MatrixRTCSession", () => { } }); - it("prunes expired memberships on update", () => { + it.skip("prunes expired memberships on update", () => { client.sendStateEvent = jest.fn(); let eventAge = 0; @@ -356,7 +356,7 @@ describe("MatrixRTCSession", () => { ); }); - it("fills in created_ts for other memberships on update", () => { + it.skip("fills in created_ts for other memberships on update", () => { client.sendStateEvent = jest.fn(); const mockRoom = makeMockRoom([ From b4c40ef705315c6a300d8bc3be9e51afd165c3dd Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 16:03:35 +0100 Subject: [PATCH 028/113] Re-jig to avoid setting listeners in the constructor and re-enable tests --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 40 +++++++++---------- .../matrixrtc/MatrixRTCSessionManager.spec.ts | 8 ++-- src/client.ts | 22 ++++++---- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 8d5e0690401..158dd5207c4 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -47,7 +47,7 @@ describe("MatrixRTCSession", () => { sess = undefined; }); - it.skip("Creates a room-scoped session from room state", () => { + it("Creates a room-scoped session from room state", () => { const mockRoom = makeMockRoom([membershipTemplate]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); @@ -59,7 +59,7 @@ describe("MatrixRTCSession", () => { expect(sess?.memberships[0].isExpired()).toEqual(false); }); - it.skip("ignores expired memberships events", () => { + it("ignores expired memberships events", () => { const expiredMembership = Object.assign({}, membershipTemplate); expiredMembership.expires = 1000; expiredMembership.device_id = "EXPIRED"; @@ -70,7 +70,7 @@ describe("MatrixRTCSession", () => { expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); }); - it.skip("honours created_ts", () => { + it("honours created_ts", () => { const expiredMembership = Object.assign({}, membershipTemplate); expiredMembership.created_ts = 500; expiredMembership.expires = 1000; @@ -79,13 +79,13 @@ describe("MatrixRTCSession", () => { expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500); }); - it.skip("returns empty session if no membership events are present", () => { + it("returns empty session if no membership events are present", () => { const mockRoom = makeMockRoom([]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); expect(sess?.memberships).toHaveLength(0); }); - it.skip("safely ignores events with no memberships section", () => { + it("safely ignores events with no memberships section", () => { const mockRoom = { roomId: randomString(8), getLiveTimeline: jest.fn().mockReturnValue({ @@ -106,7 +106,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it.skip("safely ignores events with junk memberships section", () => { + it("safely ignores events with junk memberships section", () => { const mockRoom = { roomId: randomString(8), getLiveTimeline: jest.fn().mockReturnValue({ @@ -127,7 +127,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it.skip("ignores memberships with no expires_ts", () => { + it("ignores memberships with no expires_ts", () => { const expiredMembership = Object.assign({}, membershipTemplate); (expiredMembership.expires as number | undefined) = undefined; const mockRoom = makeMockRoom([expiredMembership]); @@ -135,7 +135,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it.skip("ignores memberships with no device_id", () => { + it("ignores memberships with no device_id", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.device_id as string | undefined) = undefined; const mockRoom = makeMockRoom([testMembership]); @@ -143,7 +143,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it.skip("ignores memberships with no call_id", () => { + it("ignores memberships with no call_id", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.call_id as string | undefined) = undefined; const mockRoom = makeMockRoom([testMembership]); @@ -151,7 +151,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it.skip("ignores memberships with no scope", () => { + it("ignores memberships with no scope", () => { const testMembership = Object.assign({}, membershipTemplate); (testMembership.scope as string | undefined) = undefined; const mockRoom = makeMockRoom([testMembership]); @@ -159,7 +159,7 @@ describe("MatrixRTCSession", () => { expect(sess.memberships).toHaveLength(0); }); - it.skip("ignores anything that's not a room-scoped call (for now)", () => { + it("ignores anything that's not a room-scoped call (for now)", () => { const testMembership = Object.assign({}, membershipTemplate); testMembership.scope = "m.user"; const mockRoom = makeMockRoom([testMembership]); @@ -168,7 +168,7 @@ describe("MatrixRTCSession", () => { }); describe("getOldestMembership", () => { - it.skip("returns the oldest membership event", () => { + it("returns the oldest membership event", () => { const mockRoom = makeMockRoom([ Object.assign({}, membershipTemplate, { device_id: "foo", created_ts: 3000 }), Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }), @@ -193,16 +193,16 @@ describe("MatrixRTCSession", () => { sess!.leaveRoomSession(); }); - it.skip("starts un-joined", () => { + it("starts un-joined", () => { expect(sess!.isJoined()).toEqual(false); }); - it.skip("shows joined once join is called", () => { + it("shows joined once join is called", () => { sess!.joinRoomSession([mockFocus]); expect(sess!.isJoined()).toEqual(true); }); - it.skip("sends a membership event when joining a call", () => { + it("sends a membership event when joining a call", () => { client.sendStateEvent = jest.fn(); sess!.joinRoomSession([mockFocus]); @@ -226,7 +226,7 @@ describe("MatrixRTCSession", () => { ); }); - it.skip("does nothing if join called when already joined", () => { + it("does nothing if join called when already joined", () => { const sendStateEventMock = jest.fn(); client.sendStateEvent = sendStateEventMock; @@ -238,7 +238,7 @@ describe("MatrixRTCSession", () => { expect(client.sendStateEvent).toHaveBeenCalledTimes(1); }); - it.skip("renews membership event before expiry time", async () => { + it("renews membership event before expiry time", async () => { jest.useFakeTimers(); let resolveFn: ((_roomId: string, _type: string, val: Record) => void) | undefined; const eventSentPromise = new Promise>((r) => { @@ -289,7 +289,7 @@ describe("MatrixRTCSession", () => { }); }); - it.skip("emits an event at the time a membership event expires", () => { + it("emits an event at the time a membership event expires", () => { jest.useFakeTimers(); try { let eventAge = 0; @@ -313,7 +313,7 @@ describe("MatrixRTCSession", () => { } }); - it.skip("prunes expired memberships on update", () => { + it("prunes expired memberships on update", () => { client.sendStateEvent = jest.fn(); let eventAge = 0; @@ -356,7 +356,7 @@ describe("MatrixRTCSession", () => { ); }); - it.skip("fills in created_ts for other memberships on update", () => { + it("fills in created_ts for other memberships on update", () => { client.sendStateEvent = jest.fn(); const mockRoom = makeMockRoom([ diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index 6322d91c233..a1be7d414dd 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -41,7 +41,7 @@ describe("MatrixRTCSessionManager", () => { client.matrixRTC.stop(); }); - it.skip("Gets active MatrixRTC sessions accross multiple rooms", () => { + it("Gets active MatrixRTC sessions accross multiple rooms", () => { const room1 = makeMockRoom([membershipTemplate]); const room2 = makeMockRoom([membershipTemplate]); @@ -54,7 +54,7 @@ describe("MatrixRTCSessionManager", () => { expect(sessions).toHaveLength(2); }); - it.skip("Ignores inactive sessions", () => { + it("Ignores inactive sessions", () => { const room1 = makeMockRoom([membershipTemplate]); const room2 = makeMockRoom([]); @@ -67,7 +67,7 @@ describe("MatrixRTCSessionManager", () => { expect(sessions).toHaveLength(1); }); - it.skip("Fires event when session starts", () => { + it("Fires event when session starts", () => { const onStarted = jest.fn(); client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted); @@ -82,7 +82,7 @@ describe("MatrixRTCSessionManager", () => { } }); - it.skip("Fires event when session ends", () => { + it("Fires event when session ends", () => { const onEnded = jest.fn(); client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded); diff --git a/src/client.ts b/src/client.ts index 8c4533dce73..3938a556e0d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1340,13 +1340,12 @@ export class MatrixClient extends TypedEventEmitter { + if (this.isInitialSyncComplete()) { this.matrixRTC.start(); - this.off(ClientEvent.Sync, this.startCallEventHandlers); + this.off(ClientEvent.Sync, this.startMatrixRTC); } }; From 5b9051e63f96c04eab74012dddab0ae507864a9c Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 16:05:21 +0100 Subject: [PATCH 029/113] No need to rename this anymore --- src/client.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client.ts b/src/client.ts index 3938a556e0d..88195102a12 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1343,7 +1343,7 @@ export class MatrixClient extends TypedEventEmitter { + private startCallEventHandler = (): void => { if (this.isInitialSyncComplete()) { if (supportsMatrixCall()) { this.callEventHandler!.start(); this.groupCallEventHandler!.start(); } - this.off(ClientEvent.Sync, this.startCallEventHandlers); + this.off(ClientEvent.Sync, this.startCallEventHandler); } }; From c271625578ea8c2faedfa1e2334473c0f848c2ce Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 16:29:38 +0100 Subject: [PATCH 030/113] argh, remove the right listener --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 88195102a12..f626f69528d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1553,7 +1553,7 @@ export class MatrixClient extends TypedEventEmitter Date: Wed, 23 Aug 2023 16:39:38 +0100 Subject: [PATCH 031/113] Is it this test??? --- spec/unit/matrix-client.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index d90d3c9ed03..b65d176b815 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -144,7 +144,7 @@ describe("convertQueryDictToMap", () => { }); }); -describe("MatrixClient", function () { +describe.skip("MatrixClient", function () { const userId = "@alice:bar"; const identityServerUrl = "https://identity.server"; const identityServerDomain = "identity.server"; From 591df955ef224ff87bfd9de36002d18ae08991ff Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 17:03:47 +0100 Subject: [PATCH 032/113] Re-enable some tests --- spec/unit/matrix-client.spec.ts | 70 ++++++++++++++++----------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index b65d176b815..565a3915a24 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -144,7 +144,7 @@ describe("convertQueryDictToMap", () => { }); }); -describe.skip("MatrixClient", function () { +describe("MatrixClient", function () { const userId = "@alice:bar"; const identityServerUrl = "https://identity.server"; const identityServerDomain = "identity.server"; @@ -528,7 +528,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("getSafeUserId()", () => { + describe.skip("getSafeUserId()", () => { it("returns the logged in user id", () => { expect(client.getSafeUserId()).toEqual(userId); }); @@ -888,7 +888,7 @@ describe.skip("MatrixClient", function () { await syncPromise; }); - describe("getSyncState", function () { + describe.skip("getSyncState", function () { it("should return null if the client isn't started", function () { expect(client.getSyncState()).toBe(null); }); @@ -908,7 +908,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("getOrCreateFilter", function () { + describe.skip("getOrCreateFilter", function () { it("should POST createFilter if no id is present in localStorage", function () {}); it("should use an existing filter if id is present in localStorage", function () {}); it("should handle localStorage filterId missing from the server", async () => { @@ -942,7 +942,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("retryImmediately", function () { + describe.skip("retryImmediately", function () { it("should return false if there is no request waiting", async function () { httpLookups = []; await client.startClient(); @@ -1046,7 +1046,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("emitted sync events", function () { + describe.skip("emitted sync events", function () { function syncChecker(expectedStates: [string, string | null][], done: Function) { return function syncListener(state: SyncState, old: SyncState | null) { const expected = expectedStates.shift(); @@ -1235,7 +1235,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("inviteByEmail", function () { + describe.skip("inviteByEmail", function () { const roomId = "!foo:bar"; it("should send an invite HTTP POST", function () { @@ -1256,7 +1256,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("guest rooms", function () { + describe.skip("guest rooms", function () { it("should only do /sync calls (without filter/pushrules)", async function () { httpLookups = []; // no /pushrules or /filter httpLookups.push({ @@ -1272,7 +1272,7 @@ describe.skip("MatrixClient", function () { it.skip("should be able to peek into a room using peekInRoom", function () {}); }); - describe("getPresence", function () { + describe.skip("getPresence", function () { it("should send a presence HTTP GET", function () { httpLookups = [ { @@ -1289,7 +1289,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("redactEvent", () => { + describe.skip("redactEvent", () => { const roomId = "!room:example.org"; const mockRoom = { getMyMembership: () => "join", @@ -1376,7 +1376,7 @@ describe.skip("MatrixClient", function () { await client.redactEvent(roomId, eventId, txnId, { reason }); }); - describe("when calling with 'with_rel_types'", () => { + describe.skip("when calling with 'with_rel_types'", () => { const eventId = "$event42:example.org"; it("should raise an error if the server has no support for relation based redactions", async () => { @@ -1426,7 +1426,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("cancelPendingEvent", () => { + describe.skip("cancelPendingEvent", () => { const roomId = "!room:server"; const txnId = "m12345"; @@ -1508,7 +1508,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("threads", () => { + describe.skip("threads", () => { it.each([ { startOpts: {}, hasThreadSupport: false }, { startOpts: { threadSupport: true }, hasThreadSupport: true }, @@ -1555,7 +1555,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("read-markers and read-receipts", () => { + describe.skip("read-markers and read-receipts", () => { it("setRoomReadMarkers", () => { client.setRoomReadMarkersHttpRequest = jest.fn(); const room = { @@ -1590,7 +1590,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("beacons", () => { + describe.skip("beacons", () => { const roomId = "!room:server.org"; const content = makeBeaconInfoContent(100, true); @@ -1625,7 +1625,7 @@ describe.skip("MatrixClient", function () { expect(requestContent).toEqual(content); }); - describe("processBeaconEvents()", () => { + describe.skip("processBeaconEvents()", () => { it("does nothing when events is falsy", () => { const room = new Room(roomId, client, userId); const roomStateProcessSpy = jest.spyOn(room.currentState, "processBeaconEvents"); @@ -1655,7 +1655,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("setRoomTopic", () => { + describe.skip("setRoomTopic", () => { const roomId = "!foofoofoofoofoofoo:matrix.org"; const createSendStateEventMock = (topic: string, htmlTopic?: string) => { return jest.fn().mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => { @@ -1689,7 +1689,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("setPassword", () => { + describe.skip("setPassword", () => { const auth = { session: "abcdef", type: "foo" }; const newPassword = "newpassword"; @@ -1736,7 +1736,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("getLocalAliases", () => { + describe.skip("getLocalAliases", () => { it("should call the right endpoint", async () => { const response = { aliases: ["#woop:example.org", "#another:example.org"], @@ -1757,7 +1757,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("pollingTurnServers", () => { + describe.skip("pollingTurnServers", () => { afterEach(() => { mocked(supportsMatrixCall).mockReset(); }); @@ -1782,7 +1782,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("checkTurnServers", () => { + describe.skip("checkTurnServers", () => { beforeAll(() => { mocked(supportsMatrixCall).mockReturnValue(true); }); @@ -1850,7 +1850,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("encryptAndSendToDevices", () => { + describe.skip("encryptAndSendToDevices", () => { it("throws an error if crypto is unavailable", () => { client.crypto = undefined; expect(() => client.encryptAndSendToDevices([], {})).toThrow(); @@ -1865,7 +1865,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("support for ignoring invites", () => { + describe.skip("support for ignoring invites", () => { beforeEach(() => { // Mockup `getAccountData`/`setAccountData`. const dataStore = new Map(); @@ -2163,7 +2163,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("using E2EE in group calls", () => { + describe.skip("using E2EE in group calls", () => { const opts = { baseUrl: "https://my.home.server", idBaseUrl: identityServerUrl, @@ -2198,7 +2198,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("delete account data", () => { + describe.skip("delete account data", () => { afterEach(() => { jest.spyOn(featureUtils, "buildFeatureSupportMap").mockRestore(); }); @@ -2262,7 +2262,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("room lists and history", () => { + describe.skip("room lists and history", () => { function roomCreateEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent { return new MatrixEvent({ content: { @@ -2312,7 +2312,7 @@ describe.skip("MatrixClient", function () { }); } - describe("getVisibleRooms", () => { + describe.skip("getVisibleRooms", () => { function setUpReplacedRooms(): { room1: Room; room2: Room; @@ -2463,7 +2463,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("getRoomUpgradeHistory", () => { + describe.skip("getRoomUpgradeHistory", () => { /** * Create a chain of room history with create events and tombstones. * @@ -2713,7 +2713,7 @@ describe.skip("MatrixClient", function () { }); // these wrappers are deprecated, but we need coverage of them to pass the quality gate - describe("SecretStorage wrappers", () => { + describe.skip("SecretStorage wrappers", () => { let mockSecretStorage: Mocked; beforeEach(() => { @@ -2751,8 +2751,8 @@ describe.skip("MatrixClient", function () { }); // these wrappers are deprecated, but we need coverage of them to pass the quality gate - describe("Crypto wrappers", () => { - describe("exception if no crypto", () => { + describe.skip("Crypto wrappers", () => { + describe.skip("exception if no crypto", () => { it("isCrossSigningReady", () => { expect(() => client.isCrossSigningReady()).toThrow("End-to-end encryption disabled"); }); @@ -2766,7 +2766,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("defer to crypto backend", () => { + describe.skip("defer to crypto backend", () => { let mockCryptoBackend: Mocked; beforeEach(() => { @@ -2804,8 +2804,8 @@ describe.skip("MatrixClient", function () { }); }); - describe("paginateEventTimeline()", () => { - describe("notifications timeline", () => { + describe.skip("paginateEventTimeline()", () => { + describe.skip("notifications timeline", () => { const unsafeNotification = { actions: ["notify"], room_id: "__proto__", @@ -2964,7 +2964,7 @@ describe.skip("MatrixClient", function () { }); }); - describe("pushers", () => { + describe.skip("pushers", () => { const pusher = { app_id: "test", app_display_name: "Test App", From 5145b44052543bd69fe9d58f830e64d16a9c4003 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 17:16:59 +0100 Subject: [PATCH 033/113] Try mocking getRooms to return something valid --- spec/unit/matrix-client.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 565a3915a24..d8c64cc01d6 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -312,7 +312,6 @@ describe("MatrixClient", function () { store = ( [ "getRoom", - "getRooms", "getUser", "getSyncToken", "scrollback", @@ -339,6 +338,7 @@ describe("MatrixClient", function () { store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null)); store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null)); store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true)); + store.getRooms = jest.fn().mockReturnValue([]); // set unstableFeatures to a defined state before each test unstableFeatures = { From 1c96fc8a98ab72f12fa71a74cee24439611bf0f5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 17:25:22 +0100 Subject: [PATCH 034/113] Re-enable other tests --- spec/unit/matrix-client.spec.ts | 78 ++++++++++++++++----------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index d8c64cc01d6..abe296dd8bc 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -528,7 +528,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("getSafeUserId()", () => { + describe("getSafeUserId()", () => { it("returns the logged in user id", () => { expect(client.getSafeUserId()).toEqual(userId); }); @@ -888,7 +888,7 @@ describe("MatrixClient", function () { await syncPromise; }); - describe.skip("getSyncState", function () { + describe("getSyncState", function () { it("should return null if the client isn't started", function () { expect(client.getSyncState()).toBe(null); }); @@ -908,7 +908,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("getOrCreateFilter", function () { + describe("getOrCreateFilter", function () { it("should POST createFilter if no id is present in localStorage", function () {}); it("should use an existing filter if id is present in localStorage", function () {}); it("should handle localStorage filterId missing from the server", async () => { @@ -942,7 +942,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("retryImmediately", function () { + describe("retryImmediately", function () { it("should return false if there is no request waiting", async function () { httpLookups = []; await client.startClient(); @@ -1046,7 +1046,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("emitted sync events", function () { + describe("emitted sync events", function () { function syncChecker(expectedStates: [string, string | null][], done: Function) { return function syncListener(state: SyncState, old: SyncState | null) { const expected = expectedStates.shift(); @@ -1096,7 +1096,7 @@ describe("MatrixClient", function () { // Disabled because now `startClient` makes a legit call to `/versions` // And those tests are really unhappy about it... Not possible to figure // out what a good resolution would look like - it.skip("should transition ERROR -> CATCHUP after /sync if prev failed", async () => { + it("should transition ERROR -> CATCHUP after /sync if prev failed", async () => { const expectedStates: [string, string | null][] = []; acceptKeepalives = false; httpLookups = []; @@ -1144,7 +1144,7 @@ describe("MatrixClient", function () { await didSyncPromise; }); - it.skip("should transition SYNCING -> ERROR after a failed /sync", async () => { + it("should transition SYNCING -> ERROR after a failed /sync", async () => { acceptKeepalives = false; const expectedStates: [string, string | null][] = []; httpLookups.push({ @@ -1169,7 +1169,7 @@ describe("MatrixClient", function () { await didSyncPromise; }); - it.skip("should transition ERROR -> SYNCING after /sync if prev failed", async () => { + it("should transition ERROR -> SYNCING after /sync if prev failed", async () => { const expectedStates: [string, string | null][] = []; httpLookups.push({ method: "GET", @@ -1203,7 +1203,7 @@ describe("MatrixClient", function () { await didSyncPromise; }); - it.skip("should transition ERROR -> ERROR if keepalive keeps failing", async () => { + it("should transition ERROR -> ERROR if keepalive keeps failing", async () => { acceptKeepalives = false; const expectedStates: [string, string | null][] = []; httpLookups.push({ @@ -1235,7 +1235,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("inviteByEmail", function () { + describe("inviteByEmail", function () { const roomId = "!foo:bar"; it("should send an invite HTTP POST", function () { @@ -1256,7 +1256,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("guest rooms", function () { + describe("guest rooms", function () { it("should only do /sync calls (without filter/pushrules)", async function () { httpLookups = []; // no /pushrules or /filter httpLookups.push({ @@ -1269,10 +1269,10 @@ describe("MatrixClient", function () { expect(httpLookups.length).toBe(0); }); - it.skip("should be able to peek into a room using peekInRoom", function () {}); + it("should be able to peek into a room using peekInRoom", function () {}); }); - describe.skip("getPresence", function () { + describe("getPresence", function () { it("should send a presence HTTP GET", function () { httpLookups = [ { @@ -1289,7 +1289,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("redactEvent", () => { + describe("redactEvent", () => { const roomId = "!room:example.org"; const mockRoom = { getMyMembership: () => "join", @@ -1376,7 +1376,7 @@ describe("MatrixClient", function () { await client.redactEvent(roomId, eventId, txnId, { reason }); }); - describe.skip("when calling with 'with_rel_types'", () => { + describe("when calling with 'with_rel_types'", () => { const eventId = "$event42:example.org"; it("should raise an error if the server has no support for relation based redactions", async () => { @@ -1426,7 +1426,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("cancelPendingEvent", () => { + describe("cancelPendingEvent", () => { const roomId = "!room:server"; const txnId = "m12345"; @@ -1508,7 +1508,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("threads", () => { + describe("threads", () => { it.each([ { startOpts: {}, hasThreadSupport: false }, { startOpts: { threadSupport: true }, hasThreadSupport: true }, @@ -1555,7 +1555,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("read-markers and read-receipts", () => { + describe("read-markers and read-receipts", () => { it("setRoomReadMarkers", () => { client.setRoomReadMarkersHttpRequest = jest.fn(); const room = { @@ -1590,7 +1590,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("beacons", () => { + describe("beacons", () => { const roomId = "!room:server.org"; const content = makeBeaconInfoContent(100, true); @@ -1625,7 +1625,7 @@ describe("MatrixClient", function () { expect(requestContent).toEqual(content); }); - describe.skip("processBeaconEvents()", () => { + describe("processBeaconEvents()", () => { it("does nothing when events is falsy", () => { const room = new Room(roomId, client, userId); const roomStateProcessSpy = jest.spyOn(room.currentState, "processBeaconEvents"); @@ -1655,7 +1655,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("setRoomTopic", () => { + describe("setRoomTopic", () => { const roomId = "!foofoofoofoofoofoo:matrix.org"; const createSendStateEventMock = (topic: string, htmlTopic?: string) => { return jest.fn().mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => { @@ -1689,7 +1689,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("setPassword", () => { + describe("setPassword", () => { const auth = { session: "abcdef", type: "foo" }; const newPassword = "newpassword"; @@ -1736,7 +1736,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("getLocalAliases", () => { + describe("getLocalAliases", () => { it("should call the right endpoint", async () => { const response = { aliases: ["#woop:example.org", "#another:example.org"], @@ -1757,7 +1757,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("pollingTurnServers", () => { + describe("pollingTurnServers", () => { afterEach(() => { mocked(supportsMatrixCall).mockReset(); }); @@ -1782,7 +1782,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("checkTurnServers", () => { + describe("checkTurnServers", () => { beforeAll(() => { mocked(supportsMatrixCall).mockReturnValue(true); }); @@ -1850,7 +1850,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("encryptAndSendToDevices", () => { + describe("encryptAndSendToDevices", () => { it("throws an error if crypto is unavailable", () => { client.crypto = undefined; expect(() => client.encryptAndSendToDevices([], {})).toThrow(); @@ -1865,7 +1865,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("support for ignoring invites", () => { + describe("support for ignoring invites", () => { beforeEach(() => { // Mockup `getAccountData`/`setAccountData`. const dataStore = new Map(); @@ -2163,7 +2163,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("using E2EE in group calls", () => { + describe("using E2EE in group calls", () => { const opts = { baseUrl: "https://my.home.server", idBaseUrl: identityServerUrl, @@ -2198,7 +2198,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("delete account data", () => { + describe("delete account data", () => { afterEach(() => { jest.spyOn(featureUtils, "buildFeatureSupportMap").mockRestore(); }); @@ -2262,7 +2262,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("room lists and history", () => { + describe("room lists and history", () => { function roomCreateEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent { return new MatrixEvent({ content: { @@ -2312,7 +2312,7 @@ describe("MatrixClient", function () { }); } - describe.skip("getVisibleRooms", () => { + describe("getVisibleRooms", () => { function setUpReplacedRooms(): { room1: Room; room2: Room; @@ -2463,7 +2463,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("getRoomUpgradeHistory", () => { + describe("getRoomUpgradeHistory", () => { /** * Create a chain of room history with create events and tombstones. * @@ -2713,7 +2713,7 @@ describe("MatrixClient", function () { }); // these wrappers are deprecated, but we need coverage of them to pass the quality gate - describe.skip("SecretStorage wrappers", () => { + describe("SecretStorage wrappers", () => { let mockSecretStorage: Mocked; beforeEach(() => { @@ -2751,8 +2751,8 @@ describe("MatrixClient", function () { }); // these wrappers are deprecated, but we need coverage of them to pass the quality gate - describe.skip("Crypto wrappers", () => { - describe.skip("exception if no crypto", () => { + describe("Crypto wrappers", () => { + describe("exception if no crypto", () => { it("isCrossSigningReady", () => { expect(() => client.isCrossSigningReady()).toThrow("End-to-end encryption disabled"); }); @@ -2766,7 +2766,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("defer to crypto backend", () => { + describe("defer to crypto backend", () => { let mockCryptoBackend: Mocked; beforeEach(() => { @@ -2804,8 +2804,8 @@ describe("MatrixClient", function () { }); }); - describe.skip("paginateEventTimeline()", () => { - describe.skip("notifications timeline", () => { + describe("paginateEventTimeline()", () => { + describe("notifications timeline", () => { const unsafeNotification = { actions: ["notify"], room_id: "__proto__", @@ -2964,7 +2964,7 @@ describe("MatrixClient", function () { }); }); - describe.skip("pushers", () => { + describe("pushers", () => { const pusher = { app_id: "test", app_display_name: "Test App", From 6811ba47ea50df67f76a2bef8b1d616757e44c76 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 18:06:10 +0100 Subject: [PATCH 035/113] Give up trying to get the tests to work sensibly and deal with getRooms() returning nothing --- spec/unit/matrix-client.spec.ts | 2 +- src/matrixrtc/MatrixRTCSessonManager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index abe296dd8bc..8a66f0064cf 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -312,6 +312,7 @@ describe("MatrixClient", function () { store = ( [ "getRoom", + "getRooms", "getUser", "getSyncToken", "scrollback", @@ -338,7 +339,6 @@ describe("MatrixClient", function () { store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null)); store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null)); store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true)); - store.getRooms = jest.fn().mockReturnValue([]); // set unstableFeatures to a defined state before each test unstableFeatures = { diff --git a/src/matrixrtc/MatrixRTCSessonManager.ts b/src/matrixrtc/MatrixRTCSessonManager.ts index 93161684f59..9e41913f147 100644 --- a/src/matrixrtc/MatrixRTCSessonManager.ts +++ b/src/matrixrtc/MatrixRTCSessonManager.ts @@ -45,7 +45,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter 0) { this.roomSessions.set(room.roomId, session); From b18ae3807a3c59a98540f49b2093119597d2020a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 18:22:08 +0100 Subject: [PATCH 036/113] Oops, don't enable the ones that were skipped before --- spec/unit/matrix-client.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 8a66f0064cf..d90d3c9ed03 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -1096,7 +1096,7 @@ describe("MatrixClient", function () { // Disabled because now `startClient` makes a legit call to `/versions` // And those tests are really unhappy about it... Not possible to figure // out what a good resolution would look like - it("should transition ERROR -> CATCHUP after /sync if prev failed", async () => { + it.skip("should transition ERROR -> CATCHUP after /sync if prev failed", async () => { const expectedStates: [string, string | null][] = []; acceptKeepalives = false; httpLookups = []; @@ -1144,7 +1144,7 @@ describe("MatrixClient", function () { await didSyncPromise; }); - it("should transition SYNCING -> ERROR after a failed /sync", async () => { + it.skip("should transition SYNCING -> ERROR after a failed /sync", async () => { acceptKeepalives = false; const expectedStates: [string, string | null][] = []; httpLookups.push({ @@ -1169,7 +1169,7 @@ describe("MatrixClient", function () { await didSyncPromise; }); - it("should transition ERROR -> SYNCING after /sync if prev failed", async () => { + it.skip("should transition ERROR -> SYNCING after /sync if prev failed", async () => { const expectedStates: [string, string | null][] = []; httpLookups.push({ method: "GET", @@ -1203,7 +1203,7 @@ describe("MatrixClient", function () { await didSyncPromise; }); - it("should transition ERROR -> ERROR if keepalive keeps failing", async () => { + it.skip("should transition ERROR -> ERROR if keepalive keeps failing", async () => { acceptKeepalives = false; const expectedStates: [string, string | null][] = []; httpLookups.push({ @@ -1269,7 +1269,7 @@ describe("MatrixClient", function () { expect(httpLookups.length).toBe(0); }); - it("should be able to peek into a room using peekInRoom", function () {}); + it.skip("should be able to peek into a room using peekInRoom", function () {}); }); describe("getPresence", function () { From 40fb4ab2647a973c7b3cab73570413905cc578c5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 18:29:32 +0100 Subject: [PATCH 037/113] One more try at the sensible way --- spec/unit/matrix-client.spec.ts | 2 +- src/matrixrtc/MatrixRTCSessonManager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index d90d3c9ed03..147484c344e 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -312,7 +312,6 @@ describe("MatrixClient", function () { store = ( [ "getRoom", - "getRooms", "getUser", "getSyncToken", "scrollback", @@ -333,6 +332,7 @@ describe("MatrixClient", function () { r[k] = jest.fn(); return r; }, {} as Store); + store.getRooms = jest.fn().mockReturnValue(Promise.resolve(null)); store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null)); store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null)); store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null)); diff --git a/src/matrixrtc/MatrixRTCSessonManager.ts b/src/matrixrtc/MatrixRTCSessonManager.ts index 9e41913f147..93161684f59 100644 --- a/src/matrixrtc/MatrixRTCSessonManager.ts +++ b/src/matrixrtc/MatrixRTCSessonManager.ts @@ -45,7 +45,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter 0) { this.roomSessions.set(room.roomId, session); From 50da89632375cb1aeeeec537b8c2458dceb82886 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 23 Aug 2023 18:37:53 +0100 Subject: [PATCH 038/113] Didn't work, go back to the hack way. --- spec/unit/matrix-client.spec.ts | 2 +- src/matrixrtc/MatrixRTCSessonManager.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 147484c344e..d90d3c9ed03 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -312,6 +312,7 @@ describe("MatrixClient", function () { store = ( [ "getRoom", + "getRooms", "getUser", "getSyncToken", "scrollback", @@ -332,7 +333,6 @@ describe("MatrixClient", function () { r[k] = jest.fn(); return r; }, {} as Store); - store.getRooms = jest.fn().mockReturnValue(Promise.resolve(null)); store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null)); store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null)); store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null)); diff --git a/src/matrixrtc/MatrixRTCSessonManager.ts b/src/matrixrtc/MatrixRTCSessonManager.ts index 93161684f59..03c913932bf 100644 --- a/src/matrixrtc/MatrixRTCSessonManager.ts +++ b/src/matrixrtc/MatrixRTCSessonManager.ts @@ -45,7 +45,9 @@ export class MatrixRTCSessionManager extends TypedEventEmitter 0) { this.roomSessions.set(room.roomId, session); From f612b76a7fe4cce8734bedda03489d1de4f1c30a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 24 Aug 2023 17:50:41 +0100 Subject: [PATCH 039/113] Log when we manage to send the member event update --- src/matrixrtc/MatrixRTCSession.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 64cb34e62e9..bebf8deed8d 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -333,6 +333,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Fri, 25 Aug 2023 17:14:13 +0200 Subject: [PATCH 040/113] Support `getOpenIdToken()` in embedded mode (#3676) --- spec/unit/embedded.spec.ts | 15 +++++++++++++++ src/embedded.ts | 21 ++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index caab40ac056..fb89c47e63c 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -33,6 +33,13 @@ import { MatrixEvent } from "../../src/models/event"; import { ToDeviceBatch } from "../../src/models/ToDeviceMessage"; import { DeviceInfo } from "../../src/crypto/deviceinfo"; +const testOIDCToken = { + access_token: "12345678", + expires_in: "10", + matrix_server_name: "homeserver.oabc", + token_type: "Bearer", +}; + class MockWidgetApi extends EventEmitter { public start = jest.fn(); public requestCapability = jest.fn(); @@ -49,6 +56,7 @@ class MockWidgetApi extends EventEmitter { public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` })); public sendStateEvent = jest.fn(); public sendToDevice = jest.fn(); + public requestOpenIDConnectToken = jest.fn(() => testOIDCToken); public readStateEvents = jest.fn(() => []); public getTurnServers = jest.fn(() => []); @@ -286,6 +294,13 @@ describe("RoomWidgetClient", () => { }); }); + describe("oidc token", () => { + it("requests an oidc token", async () => { + await makeClient({}); + expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken); + }); + }); + it("gets TURN servers", async () => { const server1: ITurnServer = { uris: [ diff --git a/src/embedded.ts b/src/embedded.ts index e20c64a28b4..9ad03f2ddd4 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -29,7 +29,14 @@ import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event"; import { ISendEventResponse } from "./@types/requests"; import { EventType } from "./@types/event"; import { logger } from "./logger"; -import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts, SendToDeviceContentMap } from "./client"; +import { + MatrixClient, + ClientEvent, + IMatrixClientCreateOpts, + IStartClientOpts, + SendToDeviceContentMap, + IOpenIDToken, +} from "./client"; import { SyncApi, SyncState } from "./sync"; import { SlidingSyncSdk } from "./sliding-sync-sdk"; import { User } from "./models/user"; @@ -241,6 +248,18 @@ export class RoomWidgetClient extends MatrixClient { return {}; } + public async getOpenIdToken(): Promise { + const token = await this.widgetApi.requestOpenIDConnectToken(); + // the IOpenIDCredentials from the widget-api and IOpenIDToken form the matrix-js-sdk are compatible. + // we still recreate the token to make this transparent and catch'able by the linter in case the types change in the future. + return { + access_token: token.access_token, + expires_in: token.expires_in, + matrix_server_name: token.matrix_server_name, + token_type: token.token_type, + }; + } + public async queueToDevice({ eventType, batch }: ToDeviceBatch): Promise { // map: user Id → device Id → payload const contentMap: MapWithDefault> = new MapWithDefault(() => new Map()); From 2047c98787ce58d320067bf07e6e8865de75e2b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 25 Aug 2023 17:20:13 +0200 Subject: [PATCH 041/113] Call `sendContentLoaded()` (#3677) --- src/embedded.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/embedded.ts b/src/embedded.ts index 9ad03f2ddd4..f4f02b7ba38 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -160,6 +160,12 @@ export class RoomWidgetClient extends MatrixClient { // Open communication with the host widgetApi.start(); + // Send a content loaded event now we've started the widget API + // Note that element-web currently does not use waitForIFrameLoad=false and so + // does *not* (yes, that is the right way around) wait for this event. Let's + // start sending this, then once this has rolled out, we can change element-web to + // use waitForIFrameLoad=false and have a widget API that's less racy. + widgetApi.sendContentLoaded(); } public async startClient(opts: IStartClientOpts = {}): Promise { From 1a0718fc66d7f5684a1763c5fee9b9f5d0dafbb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 28 Aug 2023 16:58:48 +0200 Subject: [PATCH 042/113] Start MatrixRTC in embedded mode (#3679) --- src/embedded.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/embedded.ts b/src/embedded.ts index f4f02b7ba38..178d74b2ec7 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -210,6 +210,8 @@ export class RoomWidgetClient extends MatrixClient { this.setSyncState(SyncState.Syncing); logger.info("Finished backfilling events"); + this.matrixRTC.start(); + // Watch for TURN servers, if requested if (this.capabilities.turnServers) this.watchTurnServers(); } From c444e374407cee40643438e01270e1e6e2835e82 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 29 Aug 2023 14:18:07 +0100 Subject: [PATCH 043/113] Reschedule the membership event check --- src/matrixrtc/MatrixRTCSession.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index bebf8deed8d..2307e457238 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -24,6 +24,7 @@ import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000; +const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event const CALL_MEMBER_EVENT_RETRY_DELAY_MIN = 3000; export enum MatrixRTCSessionEvent { @@ -288,7 +289,11 @@ export class MatrixRTCSession extends TypedEventEmitter { let membershipObj; @@ -335,8 +340,8 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Tue, 29 Aug 2023 14:22:20 +0100 Subject: [PATCH 044/113] Bump widget api version --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2f5f3716622..215e535fc47 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "jwt-decode": "^3.1.2", "loglevel": "^1.7.1", "matrix-events-sdk": "0.0.1", - "matrix-widget-api": "^1.5.0", + "matrix-widget-api": "^1.6.0", "oidc-client-ts": "^2.2.4", "p-retry": "4", "sdp-transform": "^2.14.1", diff --git a/yarn.lock b/yarn.lock index d737daa22ac..32af96d40dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5323,10 +5323,10 @@ matrix-mock-request@^2.5.0: dependencies: expect "^28.1.0" -matrix-widget-api@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.5.0.tgz#4ae3e46a7f2854f944ddaf8a5af63d72fba76c45" - integrity sha512-hKGfqQKK5qVMwW0Sp8l2TiuW8UuHafTvUZNSWBPghedB/rSFbVLlr0mufuEV0iq/pQ7ChW96q/WEC6Llie4SnA== +matrix-widget-api@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.6.0.tgz#f0075411edffc6de339580ade7e6e6e6edb01af4" + integrity sha512-VXIJyAZ/WnBmT4C7ePqevgMYGneKMCP/0JuCOqntSsaNlCRHJvwvTxmqUU+ufOpzIF5gYNyIrAjbgrEbK3iqJQ== dependencies: "@types/events" "^3.0.0" events "^3.2.0" From e690b71d378603d533c01c18b42dd896ac8001c4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 29 Aug 2023 14:33:17 +0100 Subject: [PATCH 045/113] Add mock for sendContentLoaded() --- spec/unit/embedded.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index fb89c47e63c..20c002eecfb 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -59,6 +59,7 @@ class MockWidgetApi extends EventEmitter { public requestOpenIDConnectToken = jest.fn(() => testOIDCToken); public readStateEvents = jest.fn(() => []); public getTurnServers = jest.fn(() => []); + public sendContentLoaded = jest.fn(); public transport = { reply: jest.fn() }; } From a2713693fe755b4efe93c1b9d16fa367b10543e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 21 Aug 2023 19:19:55 +0200 Subject: [PATCH 046/113] Embeded mode pre-requisites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/@types/event.ts | 1 + src/client.ts | 1 + src/models/room-state.ts | 10 ++++++++++ src/models/room.ts | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/src/@types/event.ts b/src/@types/event.ts index 14b5b64023d..1e05bec37e0 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -55,6 +55,7 @@ export enum EventType { CallReplaces = "m.call.replaces", CallAssertedIdentity = "m.call.asserted_identity", CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity", + CallEncryptionPrefix = "io.element.call.encryption_key", KeyVerificationRequest = "m.key.verification.request", KeyVerificationStart = "m.key.verification.start", KeyVerificationCancel = "m.key.verification.cancel", diff --git a/src/client.ts b/src/client.ts index f626f69528d..fa9765e09bb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -908,6 +908,7 @@ type RoomEvents = type RoomStateEvents = | RoomStateEvent.Events | RoomStateEvent.Members + | RoomStateEvent.NoLongerMember | RoomStateEvent.NewMember | RoomStateEvent.Update | RoomStateEvent.Marker; diff --git a/src/models/room-state.ts b/src/models/room-state.ts index a1a13b313c5..e60ab9b1eb8 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -67,6 +67,7 @@ export enum RoomStateEvent { Events = "RoomState.events", Members = "RoomState.members", NewMember = "RoomState.newMember", + NoLongerMember = "RoomState.noLongerMember", Update = "RoomState.update", // signals batches of updates without specificity BeaconLiveness = "RoomState.BeaconLiveness", Marker = "RoomState.Marker", @@ -120,6 +121,7 @@ export type RoomStateEventHandlerMap = { * ``` */ [RoomStateEvent.NewMember]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void; + [RoomStateEvent.NoLongerMember]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void; [RoomStateEvent.Update]: (state: RoomState) => void; [RoomStateEvent.BeaconLiveness]: (state: RoomState, hasLiveBeacons: boolean) => void; [RoomStateEvent.Marker]: (event: MatrixEvent, setStateOptions?: IMarkerFoundOptions) => void; @@ -436,8 +438,12 @@ export class RoomState extends TypedEventEmitter } const member = this.getOrCreateMember(userId, event); + const oldMembership = member.membership; member.setMembershipEvent(event, this); this.updateMember(member); + if (oldMembership === "join" && ["leave", "ban"].includes(member.membership as string)) { + this.emit(RoomStateEvent.NoLongerMember, event, this, member); + } this.emit(RoomStateEvent.Members, event, this, member); } else if (event.getType() === EventType.RoomPowerLevels) { // events with unknown state keys should be ignored @@ -704,7 +710,11 @@ export class RoomState extends TypedEventEmitter } const member = this.getOrCreateMember(userId, stateEvent); + const oldMembership = member.membership; member.setMembershipEvent(stateEvent, this); + if (oldMembership === "join" && ["leave", "ban"].includes(member.membership as string)) { + this.emit(RoomStateEvent.NoLongerMember, stateEvent, this, member); + } // needed to know which members need to be stored seperately // as they are not part of the sync accumulator // this is cleared by setMembershipEvent so when it's updated through /sync diff --git a/src/models/room.ts b/src/models/room.ts index 6ee4b070f5a..975aee42343 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -153,6 +153,7 @@ export type RoomEmittedEvents = | RoomStateEvent.Events | RoomStateEvent.Members | RoomStateEvent.NewMember + | RoomStateEvent.NoLongerMember | RoomStateEvent.Update | RoomStateEvent.Marker | ThreadEvent.New @@ -304,6 +305,7 @@ export type RoomEventHandlerMap = { | RoomStateEvent.Events | RoomStateEvent.Members | RoomStateEvent.NewMember + | RoomStateEvent.NoLongerMember | RoomStateEvent.Update | RoomStateEvent.Marker | BeaconEvent.New @@ -1228,6 +1230,7 @@ export class Room extends ReadReceipt { RoomStateEvent.Events, RoomStateEvent.Members, RoomStateEvent.NewMember, + RoomStateEvent.NoLongerMember, RoomStateEvent.Update, RoomStateEvent.Marker, BeaconEvent.New, @@ -1239,6 +1242,7 @@ export class Room extends ReadReceipt { RoomStateEvent.Events, RoomStateEvent.Members, RoomStateEvent.NewMember, + RoomStateEvent.NoLongerMember, RoomStateEvent.Update, RoomStateEvent.Marker, BeaconEvent.New, From c6c6559525cb80b4638e2d00703478af05502d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 22 Aug 2023 17:23:27 +0200 Subject: [PATCH 047/113] Embeded mode E2EE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/CallMembership.ts | 38 +++++++++++++- src/matrixrtc/MatrixRTCSession.ts | 68 ++++++++++++++++++++++--- src/matrixrtc/MatrixRTCSessonManager.ts | 15 ++++++ 3 files changed, 113 insertions(+), 8 deletions(-) diff --git a/src/matrixrtc/CallMembership.ts b/src/matrixrtc/CallMembership.ts index 4414749f74f..dfa1ad38b10 100644 --- a/src/matrixrtc/CallMembership.ts +++ b/src/matrixrtc/CallMembership.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent, RoomMember } from "../matrix"; +import { logger } from "../logger"; +import { IEvent, MatrixClient, MatrixEvent, RoomMember } from "../matrix"; import { deepCompare } from "../utils"; import { Focus } from "./focus"; @@ -29,6 +30,7 @@ export interface CallMembershipData { created_ts?: number; expires: number; foci_active?: Focus[]; + encryption_key_event?: string; } export class CallMembership { @@ -36,11 +38,18 @@ export class CallMembership { return deepCompare(a.data, b.data); } - public constructor(private parentEvent: MatrixEvent, private data: CallMembershipData) { + public constructor( + private client: MatrixClient, + private parentEvent: MatrixEvent, + private data: CallMembershipData, + ) { if (typeof data.expires !== "number") throw new Error("Malformed membership: expires must be numeric"); if (typeof data.device_id !== "string") throw new Error("Malformed membership event: device_id must be string"); if (typeof data.call_id !== "string") throw new Error("Malformed membership event: call_id must be string"); if (typeof data.scope !== "string") throw new Error("Malformed membership event: scope must be string"); + if (typeof data.encryption_key_event !== "string") { + throw new Error("Malformed membership event: encryption_key_event must be string"); + } if (parentEvent.sender === null) throw new Error("Invalid parent event: sender is null"); } @@ -92,4 +101,29 @@ export class CallMembership { public getActiveFoci(): Focus[] { return this.data.foci_active ?? []; } + + public async getActiveEncryptionKey(): Promise { + const roomId = this.parentEvent.getRoomId(); + const eventId = this.data.encryption_key_event; + + if (!roomId) return; + if (!eventId) return; + + let partialEvent: Partial; + try { + partialEvent = await this.client.fetchRoomEvent(roomId, eventId); + } catch (error) { + logger.warn("Failed to fetch encryption key event", error); + return; + } + + const event = new MatrixEvent(partialEvent); + const content = event.getContent(); + const encryptionKey = content["io.element.key"]; + + if (!encryptionKey) return undefined; + if (typeof encryptionKey !== "string") return undefined; + + return encryptionKey; + } } diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 2307e457238..b7eaa1de32e 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -22,16 +22,21 @@ import { MatrixClient } from "../client"; import { EventType } from "../@types/event"; import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; +import { randomString } from "../randomstring"; const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000; const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event const CALL_MEMBER_EVENT_RETRY_DELAY_MIN = 3000; +const getNewEncryptionKey = (): string => randomString(32); + export enum MatrixRTCSessionEvent { // A member joined, left, or updated a proprty of their membership MembershipsChanged = "memberships_changed", // We joined or left the session (our own local idea of whether we are joined, separate from MembershipsChanged) JoinStateChanged = "join_state_changed", + // The key used to encrypt media has changed + ActiveEncryptionKeyChanged = "active_encryption_key", } export type MatrixRTCSessionEventHandlerMap = { @@ -40,6 +45,7 @@ export type MatrixRTCSessionEventHandlerMap = { newMemberships: CallMembership[], ) => void; [MatrixRTCSessionEvent.JoinStateChanged]: (isJoined: boolean) => void; + [MatrixRTCSessionEvent.ActiveEncryptionKeyChanged]: (activeEncryptionKey: string) => void; }; /** @@ -56,6 +62,13 @@ export class MatrixRTCSession extends TypedEventEmitter { if (this.isJoined()) { logger.info(`Already joined to session in room ${this.room.roomId}: ignoring join call`); return; @@ -156,8 +169,9 @@ export class MatrixRTCSession extends TypedEventEmitter { + if (!this.isJoined()) { + logger.info(`Not joined to session in room ${this.room.roomId}: ignoring update encryption key`); + return; + } + + await this.updateEncryptionKeyEvent(); + await this.updateCallMembershipEvent(); + } + + /** + * Re-sends the encryption key room event with a new key + */ + private async updateEncryptionKeyEvent(): Promise { + if (!this.isJoined()) return; + + logger.info(`MatrixRTCSession updating encryption key for room ${this.room.roomId}`); + + this._activeEncryptionKey = getNewEncryptionKey(); + this.activeEncryptionKeyEvent = ( + await this.client.sendEvent(this.room.roomId, EventType.CallEncryptionPrefix, { + "io.element.key": this._activeEncryptionKey, + }) + ).event_id; + + this.emit(MatrixRTCSessionEvent.ActiveEncryptionKeyChanged, this._activeEncryptionKey); + } + + /** + * Delete info about our encryption key + */ + private clearEncryptionKey(): void { + this._activeEncryptionKey = undefined; + this.activeEncryptionKeyEvent = undefined; + } + /** * Sets a timer for the soonest membership expiry */ @@ -237,6 +292,7 @@ export class MatrixRTCSession extends TypedEventEmitter { let membershipObj; try { - membershipObj = new CallMembership(myCallMemberEvent!, m); + membershipObj = new CallMembership(this.client, myCallMemberEvent!, m); } catch (e) { return false; } diff --git a/src/matrixrtc/MatrixRTCSessonManager.ts b/src/matrixrtc/MatrixRTCSessonManager.ts index 03c913932bf..c2f13c6d7af 100644 --- a/src/matrixrtc/MatrixRTCSessonManager.ts +++ b/src/matrixrtc/MatrixRTCSessonManager.ts @@ -56,6 +56,8 @@ export class MatrixRTCSessionManager extends TypedEventEmitter => { + const room = this.client.getRoom(event.getRoomId()); + if (!room) { + logger.error(`Got membership change for unknown room ${event.getRoomId()}!`); + return; + } + + const session = this.getRoomSession(room); + await session.updateEncryptionKey(); + }; + private refreshRoom(room: Room): void { const hadSession = this.roomSessions.has(room.roomId); const sess = this.getRoomSession(room); From 3799c655997df6462ab0372ede89c992e618c029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 25 Aug 2023 13:12:54 +0200 Subject: [PATCH 048/113] Encryption condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/CallMembership.ts | 2 +- src/matrixrtc/MatrixRTCSession.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/matrixrtc/CallMembership.ts b/src/matrixrtc/CallMembership.ts index dfa1ad38b10..8cba1875db5 100644 --- a/src/matrixrtc/CallMembership.ts +++ b/src/matrixrtc/CallMembership.ts @@ -47,7 +47,7 @@ export class CallMembership { if (typeof data.device_id !== "string") throw new Error("Malformed membership event: device_id must be string"); if (typeof data.call_id !== "string") throw new Error("Malformed membership event: call_id must be string"); if (typeof data.scope !== "string") throw new Error("Malformed membership event: scope must be string"); - if (typeof data.encryption_key_event !== "string") { + if (data.encryption_key_event && typeof data.encryption_key_event !== "string") { throw new Error("Malformed membership event: encryption_key_event must be string"); } if (parentEvent.sender === null) throw new Error("Invalid parent event: sender is null"); diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index b7eaa1de32e..f4c9022809f 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -160,7 +160,7 @@ export class MatrixRTCSession extends TypedEventEmitter { + public async joinRoomSession(activeFoci: Focus[], encryptMedia?: boolean): Promise { if (this.isJoined()) { logger.info(`Already joined to session in room ${this.room.roomId}: ignoring join call`); return; @@ -169,7 +169,9 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Wed, 30 Aug 2023 09:54:25 +0200 Subject: [PATCH 049/113] Revert "Embeded mode pre-requisites" This reverts commit 8cd73702052609c995ad754e31f85d0da0be4aa9. --- src/@types/event.ts | 1 - src/client.ts | 1 - src/models/room-state.ts | 10 ---------- src/models/room.ts | 4 ---- 4 files changed, 16 deletions(-) diff --git a/src/@types/event.ts b/src/@types/event.ts index 1e05bec37e0..14b5b64023d 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -55,7 +55,6 @@ export enum EventType { CallReplaces = "m.call.replaces", CallAssertedIdentity = "m.call.asserted_identity", CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity", - CallEncryptionPrefix = "io.element.call.encryption_key", KeyVerificationRequest = "m.key.verification.request", KeyVerificationStart = "m.key.verification.start", KeyVerificationCancel = "m.key.verification.cancel", diff --git a/src/client.ts b/src/client.ts index fa9765e09bb..f626f69528d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -908,7 +908,6 @@ type RoomEvents = type RoomStateEvents = | RoomStateEvent.Events | RoomStateEvent.Members - | RoomStateEvent.NoLongerMember | RoomStateEvent.NewMember | RoomStateEvent.Update | RoomStateEvent.Marker; diff --git a/src/models/room-state.ts b/src/models/room-state.ts index e60ab9b1eb8..a1a13b313c5 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -67,7 +67,6 @@ export enum RoomStateEvent { Events = "RoomState.events", Members = "RoomState.members", NewMember = "RoomState.newMember", - NoLongerMember = "RoomState.noLongerMember", Update = "RoomState.update", // signals batches of updates without specificity BeaconLiveness = "RoomState.BeaconLiveness", Marker = "RoomState.Marker", @@ -121,7 +120,6 @@ export type RoomStateEventHandlerMap = { * ``` */ [RoomStateEvent.NewMember]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void; - [RoomStateEvent.NoLongerMember]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void; [RoomStateEvent.Update]: (state: RoomState) => void; [RoomStateEvent.BeaconLiveness]: (state: RoomState, hasLiveBeacons: boolean) => void; [RoomStateEvent.Marker]: (event: MatrixEvent, setStateOptions?: IMarkerFoundOptions) => void; @@ -438,12 +436,8 @@ export class RoomState extends TypedEventEmitter } const member = this.getOrCreateMember(userId, event); - const oldMembership = member.membership; member.setMembershipEvent(event, this); this.updateMember(member); - if (oldMembership === "join" && ["leave", "ban"].includes(member.membership as string)) { - this.emit(RoomStateEvent.NoLongerMember, event, this, member); - } this.emit(RoomStateEvent.Members, event, this, member); } else if (event.getType() === EventType.RoomPowerLevels) { // events with unknown state keys should be ignored @@ -710,11 +704,7 @@ export class RoomState extends TypedEventEmitter } const member = this.getOrCreateMember(userId, stateEvent); - const oldMembership = member.membership; member.setMembershipEvent(stateEvent, this); - if (oldMembership === "join" && ["leave", "ban"].includes(member.membership as string)) { - this.emit(RoomStateEvent.NoLongerMember, stateEvent, this, member); - } // needed to know which members need to be stored seperately // as they are not part of the sync accumulator // this is cleared by setMembershipEvent so when it's updated through /sync diff --git a/src/models/room.ts b/src/models/room.ts index 975aee42343..6ee4b070f5a 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -153,7 +153,6 @@ export type RoomEmittedEvents = | RoomStateEvent.Events | RoomStateEvent.Members | RoomStateEvent.NewMember - | RoomStateEvent.NoLongerMember | RoomStateEvent.Update | RoomStateEvent.Marker | ThreadEvent.New @@ -305,7 +304,6 @@ export type RoomEventHandlerMap = { | RoomStateEvent.Events | RoomStateEvent.Members | RoomStateEvent.NewMember - | RoomStateEvent.NoLongerMember | RoomStateEvent.Update | RoomStateEvent.Marker | BeaconEvent.New @@ -1230,7 +1228,6 @@ export class Room extends ReadReceipt { RoomStateEvent.Events, RoomStateEvent.Members, RoomStateEvent.NewMember, - RoomStateEvent.NoLongerMember, RoomStateEvent.Update, RoomStateEvent.Marker, BeaconEvent.New, @@ -1242,7 +1239,6 @@ export class Room extends ReadReceipt { RoomStateEvent.Events, RoomStateEvent.Members, RoomStateEvent.NewMember, - RoomStateEvent.NoLongerMember, RoomStateEvent.Update, RoomStateEvent.Marker, BeaconEvent.New, From 72808b52a7d8fc7c16720292ec82b05de1920fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 30 Aug 2023 11:17:50 +0200 Subject: [PATCH 050/113] Get back event type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner fds Signed-off-by: Šimon Brandner --- src/@types/event.ts | 1 + src/matrixrtc/MatrixRTCSessonManager.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/@types/event.ts b/src/@types/event.ts index 14b5b64023d..1e05bec37e0 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -55,6 +55,7 @@ export enum EventType { CallReplaces = "m.call.replaces", CallAssertedIdentity = "m.call.asserted_identity", CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity", + CallEncryptionPrefix = "io.element.call.encryption_key", KeyVerificationRequest = "m.key.verification.request", KeyVerificationStart = "m.key.verification.start", KeyVerificationCancel = "m.key.verification.cancel", diff --git a/src/matrixrtc/MatrixRTCSessonManager.ts b/src/matrixrtc/MatrixRTCSessonManager.ts index c2f13c6d7af..4ab41a06e74 100644 --- a/src/matrixrtc/MatrixRTCSessonManager.ts +++ b/src/matrixrtc/MatrixRTCSessonManager.ts @@ -21,6 +21,7 @@ import { Room } from "../models/room"; import { RoomState, RoomStateEvent } from "../models/room-state"; import { MatrixEvent } from "../models/event"; import { MatrixRTCSession } from "./MatrixRTCSession"; +import { EventType } from "../@types/event"; export enum MatrixRTCSessionManagerEvents { // A member has joined the MatrixRTC session, creating an active session in a room where there wasn't previously From ebcdd161cdf695d720420e421f8aee5775d5d21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 29 Aug 2023 16:50:35 +0200 Subject: [PATCH 051/113] Change embedded E2EE implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/CallMembership.ts | 38 +--------- src/matrixrtc/MatrixRTCSession.ts | 96 ++++++++++++++----------- src/matrixrtc/MatrixRTCSessonManager.ts | 25 ++++--- src/matrixrtc/types.ts | 20 ++++++ 4 files changed, 87 insertions(+), 92 deletions(-) create mode 100644 src/matrixrtc/types.ts diff --git a/src/matrixrtc/CallMembership.ts b/src/matrixrtc/CallMembership.ts index 8cba1875db5..4414749f74f 100644 --- a/src/matrixrtc/CallMembership.ts +++ b/src/matrixrtc/CallMembership.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { logger } from "../logger"; -import { IEvent, MatrixClient, MatrixEvent, RoomMember } from "../matrix"; +import { MatrixEvent, RoomMember } from "../matrix"; import { deepCompare } from "../utils"; import { Focus } from "./focus"; @@ -30,7 +29,6 @@ export interface CallMembershipData { created_ts?: number; expires: number; foci_active?: Focus[]; - encryption_key_event?: string; } export class CallMembership { @@ -38,18 +36,11 @@ export class CallMembership { return deepCompare(a.data, b.data); } - public constructor( - private client: MatrixClient, - private parentEvent: MatrixEvent, - private data: CallMembershipData, - ) { + public constructor(private parentEvent: MatrixEvent, private data: CallMembershipData) { if (typeof data.expires !== "number") throw new Error("Malformed membership: expires must be numeric"); if (typeof data.device_id !== "string") throw new Error("Malformed membership event: device_id must be string"); if (typeof data.call_id !== "string") throw new Error("Malformed membership event: call_id must be string"); if (typeof data.scope !== "string") throw new Error("Malformed membership event: scope must be string"); - if (data.encryption_key_event && typeof data.encryption_key_event !== "string") { - throw new Error("Malformed membership event: encryption_key_event must be string"); - } if (parentEvent.sender === null) throw new Error("Invalid parent event: sender is null"); } @@ -101,29 +92,4 @@ export class CallMembership { public getActiveFoci(): Focus[] { return this.data.foci_active ?? []; } - - public async getActiveEncryptionKey(): Promise { - const roomId = this.parentEvent.getRoomId(); - const eventId = this.data.encryption_key_event; - - if (!roomId) return; - if (!eventId) return; - - let partialEvent: Partial; - try { - partialEvent = await this.client.fetchRoomEvent(roomId, eventId); - } catch (error) { - logger.warn("Failed to fetch encryption key event", error); - return; - } - - const event = new MatrixEvent(partialEvent); - const content = event.getContent(); - const encryptionKey = content["io.element.key"]; - - if (!encryptionKey) return undefined; - if (typeof encryptionKey !== "string") return undefined; - - return encryptionKey; - } } diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index f4c9022809f..a25c08880bb 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -23,6 +23,8 @@ import { EventType } from "../@types/event"; import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; import { randomString } from "../randomstring"; +import { MatrixEvent } from "../matrix"; +import { EncryptionKeyEventContent } from "./types"; const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000; const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event @@ -36,7 +38,7 @@ export enum MatrixRTCSessionEvent { // We joined or left the session (our own local idea of whether we are joined, separate from MembershipsChanged) JoinStateChanged = "join_state_changed", // The key used to encrypt media has changed - ActiveEncryptionKeyChanged = "active_encryption_key", + EncryptionKeyChanged = "encryption_key_changed", } export type MatrixRTCSessionEventHandlerMap = { @@ -45,7 +47,7 @@ export type MatrixRTCSessionEventHandlerMap = { newMemberships: CallMembership[], ) => void; [MatrixRTCSessionEvent.JoinStateChanged]: (isJoined: boolean) => void; - [MatrixRTCSessionEvent.ActiveEncryptionKeyChanged]: (activeEncryptionKey: string) => void; + [MatrixRTCSessionEvent.EncryptionKeyChanged]: (key: string, userId: string, deviceId: string) => void; }; /** @@ -62,11 +64,11 @@ export class MatrixRTCSession extends TypedEventEmitter(); - public get activeEncryptionKey(): string | undefined { - return this._activeEncryptionKey; + public get encryptionKeys(): Map<{ userId: string; deviceId: string }, string> { + return new Map(this._encryptionKeys); } /** @@ -94,7 +96,7 @@ export class MatrixRTCSession extends TypedEventEmitter { - if (!this.isJoined()) { - logger.info(`Not joined to session in room ${this.room.roomId}: ignoring update encryption key`); - return; - } - if (!this._activeEncryptionKey) return; - - await this.updateEncryptionKeyEvent(); - await this.updateCallMembershipEvent(); - } - /** * Re-sends the encryption key room event with a new key */ private async updateEncryptionKeyEvent(): Promise { if (!this.isJoined()) return; + if (!this.encryptMedia) return; logger.info(`MatrixRTCSession updating encryption key for room ${this.room.roomId}`); - this._activeEncryptionKey = getNewEncryptionKey(); - this.activeEncryptionKeyEvent = ( - await this.client.sendEvent(this.room.roomId, EventType.CallEncryptionPrefix, { - "io.element.key": this._activeEncryptionKey, - }) - ).event_id; + const encryptionKey = getNewEncryptionKey(); + await this.client.sendEvent(this.room.roomId, EventType.CallEncryptionPrefix, { + "m.encryption_key": encryptionKey, + "m.device_id": this.client.getDeviceId(), + } as EncryptionKeyEventContent); - this.emit(MatrixRTCSessionEvent.ActiveEncryptionKeyChanged, this._activeEncryptionKey); - } + const userId = this.client.getUserId(); + const deviceId = this.client.getDeviceId(); - /** - * Delete info about our encryption key - */ - private clearEncryptionKey(): void { - this._activeEncryptionKey = undefined; - this.activeEncryptionKeyEvent = undefined; + if (!userId) throw new Error("No userId"); + if (!deviceId) throw new Error("No deviceId"); + + this._encryptionKeys.set({ userId, deviceId }, encryptionKey); + this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKey, userId, deviceId); } /** @@ -263,6 +247,26 @@ export class MatrixRTCSession extends TypedEventEmitter { + const userId = event.getSender(); + const content = event.getContent(); + const encryptionKey = content["m.encryption_key"]; + const deviceId = content["m.device_id"]; + + if ( + !userId || + !deviceId || + !encryptionKey || + typeof deviceId !== "string" || + typeof encryptionKey !== "string" + ) { + throw new Error("Malformed m.call.encryption_key"); + } + + this._encryptionKeys.set({ userId, deviceId }, encryptionKey); + this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKey, userId, deviceId); + }; + public onMembershipUpdate = (): void => { const oldMemberships = this.memberships; this.memberships = MatrixRTCSession.callMembershipsForRoom(this.room); @@ -276,6 +280,13 @@ export class MatrixRTCSession extends TypedEventEmitter `${m.member.userId}:${m.deviceId}`; + const callMembersChanged = new Set(oldMemberships.map(ts)) !== new Set(this.memberships.map(ts)); + + if (callMembersChanged && this.isJoined()) { + this.updateEncryptionKeyEvent(); + } + this.setExpiryTimer(); }; @@ -297,7 +308,6 @@ export class MatrixRTCSession extends TypedEventEmitter { let membershipObj; try { - membershipObj = new CallMembership(this.client, myCallMemberEvent!, m); + membershipObj = new CallMembership(myCallMemberEvent!, m); } catch (e) { return false; } diff --git a/src/matrixrtc/MatrixRTCSessonManager.ts b/src/matrixrtc/MatrixRTCSessonManager.ts index 4ab41a06e74..71ca5495b86 100644 --- a/src/matrixrtc/MatrixRTCSessonManager.ts +++ b/src/matrixrtc/MatrixRTCSessonManager.ts @@ -17,7 +17,7 @@ limitations under the License. import { logger } from "../logger"; import { MatrixClient, ClientEvent } from "../client"; import { TypedEventEmitter } from "../models/typed-event-emitter"; -import { Room } from "../models/room"; +import { Room, RoomEvent } from "../models/room"; import { RoomState, RoomStateEvent } from "../models/room-state"; import { MatrixEvent } from "../models/event"; import { MatrixRTCSession } from "./MatrixRTCSession"; @@ -56,9 +56,8 @@ export class MatrixRTCSessionManager extends TypedEventEmitter { - this.refreshRoom(room); - }; + private onTimeline = (event: MatrixEvent): void => { + if (event.getType() !== EventType.CallEncryptionPrefix) return; - private onRoomState = (event: MatrixEvent, _state: RoomState): void => { const room = this.client.getRoom(event.getRoomId()); if (!room) { logger.error(`Got room state event for unknown room ${event.getRoomId()}!`); return; } + this.getRoomSession(room).onCallEncryption(event); + }; + + private onRoom = (room: Room): void => { this.refreshRoom(room); }; - private onRoomMembershipChange = async (event: MatrixEvent): Promise => { + private onRoomState = (event: MatrixEvent, _state: RoomState): void => { const room = this.client.getRoom(event.getRoomId()); if (!room) { - logger.error(`Got membership change for unknown room ${event.getRoomId()}!`); + logger.error(`Got room state event for unknown room ${event.getRoomId()}!`); return; } - const session = this.getRoomSession(room); - await session.updateEncryptionKey(); + this.refreshRoom(room); }; private refreshRoom(room: Room): void { diff --git a/src/matrixrtc/types.ts b/src/matrixrtc/types.ts new file mode 100644 index 00000000000..bdf49af04bc --- /dev/null +++ b/src/matrixrtc/types.ts @@ -0,0 +1,20 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export interface EncryptionKeyEventContent { + "m.encryption_key": string; + "m.device_id": string; +} From 4ea07544a19be82c0c9096e1c8a5926661a8ebec Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 31 Aug 2023 09:49:25 +0100 Subject: [PATCH 052/113] More log detail --- src/matrixrtc/MatrixRTCSession.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 2307e457238..e57082ccf32 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -71,11 +71,11 @@ export class MatrixRTCSession extends TypedEventEmitter a.createdTs() - b.createdTs()); + logger.debug( + "Call memberships, in order: ", + callMemberships.map((m) => [m.createdTs(), m.member.userId]), + ); return callMemberships; } @@ -274,6 +278,10 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 31 Aug 2023 10:11:44 +0100 Subject: [PATCH 053/113] Fix tests and also better assert because the tests were passing undefined which was considered fine because we were only checking for null. --- spec/unit/matrixrtc/CallMembership.spec.ts | 3 +++ spec/unit/matrixrtc/mocks.ts | 3 +++ src/matrixrtc/CallMembership.ts | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/unit/matrixrtc/CallMembership.spec.ts b/spec/unit/matrixrtc/CallMembership.spec.ts index 745dad3c9df..a8ae0a8224a 100644 --- a/spec/unit/matrixrtc/CallMembership.spec.ts +++ b/spec/unit/matrixrtc/CallMembership.spec.ts @@ -28,6 +28,9 @@ const membershipTemplate: CallMembershipData = { function makeMockEvent(originTs = 0): MatrixEvent { return { getTs: jest.fn().mockReturnValue(originTs), + sender: { + userId: "@alice:example.org", + }, } as unknown as MatrixEvent; } diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index e791c75861c..fa7d948e620 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -59,5 +59,8 @@ export function mockRTCEvent( getLocalAge: getLocalAgeFn, localTimestamp: Date.now(), getRoomId: jest.fn().mockReturnValue(roomId), + sender: { + userId: "@mock:user.example", + }, } as unknown as MatrixEvent; } diff --git a/src/matrixrtc/CallMembership.ts b/src/matrixrtc/CallMembership.ts index 4414749f74f..e67909d1e32 100644 --- a/src/matrixrtc/CallMembership.ts +++ b/src/matrixrtc/CallMembership.ts @@ -41,7 +41,7 @@ export class CallMembership { if (typeof data.device_id !== "string") throw new Error("Malformed membership event: device_id must be string"); if (typeof data.call_id !== "string") throw new Error("Malformed membership event: call_id must be string"); if (typeof data.scope !== "string") throw new Error("Malformed membership event: scope must be string"); - if (parentEvent.sender === null) throw new Error("Invalid parent event: sender is null"); + if (!parentEvent.sender) throw new Error("Invalid parent event: sender is null"); } public get member(): RoomMember { From eb25a28a5144d6d8f1c7815230a9a6a922426bcd Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 31 Aug 2023 11:42:40 +0100 Subject: [PATCH 054/113] Simplify updateCallMembershipEvent a bit --- src/matrixrtc/MatrixRTCSession.ts | 47 ++++++++++++++++++------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index e57082ccf32..06d2e8f5392 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -248,6 +248,31 @@ export class MatrixRTCSession extends TypedEventEmitter => { if (this.memberEventTimeout) { clearTimeout(this.memberEventTimeout); @@ -255,13 +280,12 @@ export class MatrixRTCSession extends TypedEventEmitter>() ?? {}; const memberships: CallMembershipData[] = Array.isArray(content["memberships"]) ? content["memberships"] : []; @@ -282,22 +306,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 31 Aug 2023 11:52:46 +0100 Subject: [PATCH 055/113] Split up updateCallMembershipEvent some more --- src/matrixrtc/MatrixRTCSession.ts | 82 ++++++++++++++++++------------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 06d2e8f5392..2ad8e04b544 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -22,6 +22,7 @@ import { MatrixClient } from "../client"; import { EventType } from "../@types/event"; import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; +import { MatrixEvent } from "../matrix"; const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000; const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event @@ -273,6 +274,52 @@ export class MatrixRTCSession extends TypedEventEmitter { + let membershipObj; + try { + membershipObj = new CallMembership(myCallMemberEvent!, m); + } catch (e) { + return false; + } + + return !membershipObj.isExpired(); + }; + + const transformMemberships = (m: CallMembershipData): CallMembershipData => { + if (m.created_ts === undefined) { + // we need to fill this in with the origin_server_ts from its original event + m.created_ts = myCallMemberEvent!.getTs(); + } + + return m; + }; + + // Filter our any invalid or expired memberships, and also our own - we'll add that back in next + let newMemberships = oldMemberships.filter(filterExpired).filter((m) => m.device_id !== localDeviceId); + + // Fix up any memberships that need their created_ts adding + newMemberships = newMemberships.map(transformMemberships); + + // If we're joined, add our own + if (this.isJoined()) { + newMemberships.push(this.makeMyMembership(myPrevMembership)); + } + + return newMemberships; + } + private updateCallMembershipEvent = async (): Promise => { if (this.memberEventTimeout) { clearTimeout(this.memberEventTimeout); @@ -286,7 +333,7 @@ export class MatrixRTCSession extends TypedEventEmitter>() ?? {}; const memberships: CallMembershipData[] = Array.isArray(content["memberships"]) ? content["memberships"] : []; @@ -312,39 +359,8 @@ export class MatrixRTCSession extends TypedEventEmitter { - let membershipObj; - try { - membershipObj = new CallMembership(myCallMemberEvent!, m); - } catch (e) { - return false; - } - - return !membershipObj.isExpired(); - }; - - const transformMemberships = (m: CallMembershipData): CallMembershipData => { - if (m.created_ts === undefined) { - // we need to fill this in with the origin_server_ts from its original event - m.created_ts = myCallMemberEvent!.getTs(); - } - - return m; - }; - - // Filter our any invalid or expired memberships, and also our own - we'll add that back in next - let newMemberships = memberships.filter(filterExpired).filter((m) => m.device_id !== localDeviceId); - - // Fix up any memberships that need their created_ts adding - newMemberships = newMemberships.map(transformMemberships); - - // If we're joined, add our own - if (this.isJoined()) { - newMemberships.push(this.makeMyMembership(myPrevMembership)); - } - const newContent = { - memberships: newMemberships, + memberships: this.makeNewMemberships(memberships, myCallMemberEvent, myPrevMembership), }; let resendDelay; From eb2b0caae55bdf4cf88e04ad46a82e8bb8ea1d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 31 Aug 2023 13:41:58 +0200 Subject: [PATCH 056/113] Use `crypto.getRandomValues()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index a25c08880bb..64a8bb677df 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -22,15 +22,19 @@ import { MatrixClient } from "../client"; import { EventType } from "../@types/event"; import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; -import { randomString } from "../randomstring"; import { MatrixEvent } from "../matrix"; import { EncryptionKeyEventContent } from "./types"; +import { encodeBase64 } from "../crypto/olmlib"; const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000; const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event const CALL_MEMBER_EVENT_RETRY_DELAY_MIN = 3000; -const getNewEncryptionKey = (): string => randomString(32); +const getNewEncryptionKey = (): string => { + const key = new Uint8Array(32); + crypto.getRandomValues(key); + return encodeBase64(key); +}; export enum MatrixRTCSessionEvent { // A member joined, left, or updated a proprty of their membership From 86bd66d7eedbd96d926ad7d3041a7fdfd7573c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 31 Aug 2023 13:48:22 +0200 Subject: [PATCH 057/113] Rename to `membershipToUserAndDeviceId()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 64a8bb677df..ebce8f7b7a2 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -36,6 +36,8 @@ const getNewEncryptionKey = (): string => { return encodeBase64(key); }; +const membershipToUserAndDeviceId = (m: CallMembership): string => `${m.member.userId}:${m.deviceId}`; + export enum MatrixRTCSessionEvent { // A member joined, left, or updated a proprty of their membership MembershipsChanged = "memberships_changed", @@ -284,8 +286,9 @@ export class MatrixRTCSession extends TypedEventEmitter `${m.member.userId}:${m.deviceId}`; - const callMembersChanged = new Set(oldMemberships.map(ts)) !== new Set(this.memberships.map(ts)); + const callMembersChanged = + new Set(oldMemberships.map(membershipToUserAndDeviceId)) !== + new Set(this.memberships.map(membershipToUserAndDeviceId)); if (callMembersChanged && this.isJoined()) { this.updateEncryptionKeyEvent(); From 32ee6f7e0d2b5dc86e9b12a332de9f9dc7b132d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 31 Aug 2023 13:49:25 +0200 Subject: [PATCH 058/113] Better error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index ebce8f7b7a2..be2fda88fd7 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -266,7 +266,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 31 Aug 2023 13:50:18 +0200 Subject: [PATCH 059/113] Add log line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index be2fda88fd7..db125b50029 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -271,6 +271,7 @@ export class MatrixRTCSession extends TypedEventEmitter { From 0a877e897094f6a5984b70b3824e8a8c781ceb51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 31 Aug 2023 13:53:41 +0200 Subject: [PATCH 060/113] Add comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index db125b50029..9d7c786d30b 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -73,6 +73,10 @@ export class MatrixRTCSession extends TypedEventEmitter(); + /** + * A map of keys used to encrypt and decrypt (we are using a symmetric + * cipher) given participant's media. This also includes our own key + */ public get encryptionKeys(): Map<{ userId: string; deviceId: string }, string> { return new Map(this._encryptionKeys); } From 6877c0ece31be212e3a161cafe8e135c4db7c204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 31 Aug 2023 13:56:29 +0200 Subject: [PATCH 061/113] Send call ID in enc events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (also a small refactor) Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 30 ++++++++++++++++++++++-------- src/matrixrtc/types.ts | 1 + 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 9d7c786d30b..0f16c4079c4 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -213,20 +213,21 @@ export class MatrixRTCSession extends TypedEventEmitter(); const encryptionKey = content["m.encryption_key"]; const deviceId = content["m.device_id"]; + const callId = content["m.call_id"]; if ( !userId || !deviceId || + !callId || !encryptionKey || typeof deviceId !== "string" || + typeof callId !== "string" || typeof encryptionKey !== "string" ) { - throw new Error(`Malformed m.call.encryption_key from userId=${userId}, deviceId=${deviceId}`); + throw new Error( + `Malformed m.call.encryption_key: userId=${userId}, deviceId=${deviceId}, callId=${callId}`, + ); + } + + // We currently only handle callId = "" + if (callId !== "") { + logger.warn( + `Received m.call.encryption_key with unsupported callId: userId=${userId}, deviceId=${deviceId}, callId=${callId}`, + ); + return; } this._encryptionKeys.set({ userId, deviceId }, encryptionKey); diff --git a/src/matrixrtc/types.ts b/src/matrixrtc/types.ts index bdf49af04bc..68033ab444a 100644 --- a/src/matrixrtc/types.ts +++ b/src/matrixrtc/types.ts @@ -17,4 +17,5 @@ limitations under the License. export interface EncryptionKeyEventContent { "m.encryption_key": string; "m.device_id": string; + "m.call_id": string; } From c4cf319c0b1c671b530d922e1216f51df24a9510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 31 Aug 2023 14:00:40 +0200 Subject: [PATCH 062/113] Revert making `joinRoomSession()` async MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 0f16c4079c4..900f56c44a8 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -172,7 +172,7 @@ export class MatrixRTCSession extends TypedEventEmitter { + public joinRoomSession(activeFoci: Focus[], encryptMedia?: boolean): void { if (this.isJoined()) { logger.info(`Already joined to session in room ${this.room.roomId}: ignoring join call`); return; @@ -183,7 +183,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 31 Aug 2023 14:02:07 +0200 Subject: [PATCH 063/113] Make `client` `private` again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 900f56c44a8..c751f86b954 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -142,7 +142,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 4 Sep 2023 15:59:12 +0200 Subject: [PATCH 064/113] Just use `toString()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 6467b20ce67..4679c47fad1 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -24,7 +24,6 @@ import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; import { MatrixEvent } from "../matrix"; import { EncryptionKeyEventContent } from "./types"; -import { encodeBase64 } from "../crypto/olmlib"; const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000; const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event @@ -33,7 +32,7 @@ const CALL_MEMBER_EVENT_RETRY_DELAY_MIN = 3000; const getNewEncryptionKey = (): string => { const key = new Uint8Array(32); crypto.getRandomValues(key); - return encodeBase64(key); + return key.toString(); }; const membershipToUserAndDeviceId = (m: CallMembership): string => `${m.member.userId}:${m.deviceId}`; From 6ee456eb3cb31b196ea8ded7730de61397291684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 4 Sep 2023 15:59:51 +0200 Subject: [PATCH 065/113] Fix `callId` check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 4679c47fad1..baebbc2a798 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -271,8 +271,9 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 4 Sep 2023 16:55:53 +0200 Subject: [PATCH 066/113] Fix map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index baebbc2a798..180c64e2069 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -35,7 +35,8 @@ const getNewEncryptionKey = (): string => { return key.toString(); }; -const membershipToUserAndDeviceId = (m: CallMembership): string => `${m.member.userId}:${m.deviceId}`; +const combineUserAndDeviceId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`; +const membershipToUserAndDeviceId = (m: CallMembership): string => combineUserAndDeviceId(m.member.userId, m.deviceId); export enum MatrixRTCSessionEvent { // A member joined, left, or updated a proprty of their membership @@ -70,14 +71,18 @@ export class MatrixRTCSession extends TypedEventEmitter(); + private encryptionKeys = new Map(); + + public getKeyForParticipant(userId: string, deviceId: string): string | undefined { + return this.encryptionKeys.get(combineUserAndDeviceId(userId, deviceId)); + } /** * A map of keys used to encrypt and decrypt (we are using a symmetric * cipher) given participant's media. This also includes our own key */ - public get encryptionKeys(): Map<{ userId: string; deviceId: string }, string> { - return new Map(this._encryptionKeys); + public getEncryptionKeys(): IterableIterator<[string, string]> { + return this.encryptionKeys.entries(); } /** @@ -231,7 +236,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 4 Sep 2023 18:21:02 +0200 Subject: [PATCH 067/113] Fix map compare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 180c64e2069..6a929bdaa3b 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -315,8 +315,8 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 4 Sep 2023 18:21:34 +0200 Subject: [PATCH 068/113] Fix emitting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 6a929bdaa3b..ffed1789a67 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -53,7 +53,7 @@ export type MatrixRTCSessionEventHandlerMap = { newMemberships: CallMembership[], ) => void; [MatrixRTCSessionEvent.JoinStateChanged]: (isJoined: boolean) => void; - [MatrixRTCSessionEvent.EncryptionKeyChanged]: (key: string, userId: string, deviceId: string) => void; + [MatrixRTCSessionEvent.EncryptionKeyChanged]: (key: string, participantId: string) => void; }; /** @@ -236,8 +236,9 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Wed, 6 Sep 2023 08:18:27 +0200 Subject: [PATCH 069/113] Explicit logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index ffed1789a67..a1fa0fbdcc1 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -227,8 +227,6 @@ export class MatrixRTCSession extends TypedEventEmitter { From 451d26e726ad40fd902a8bd755c702694288a7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 7 Sep 2023 17:01:09 +0200 Subject: [PATCH 070/113] Refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index a1fa0fbdcc1..173fd683535 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -73,6 +73,14 @@ export class MatrixRTCSession extends TypedEventEmitter(); + private setEncryptionKey(userId: string, deviceId: string, encryptionKey: string): void { + const participantId = combineUserAndDeviceId(userId, deviceId); + if (this.encryptionKeys.get(participantId) === encryptionKey) return; + + this.encryptionKeys.set(participantId, encryptionKey); + this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKey, participantId); + } + public getKeyForParticipant(userId: string, deviceId: string): string | undefined { return this.encryptionKeys.get(combineUserAndDeviceId(userId, deviceId)); } @@ -234,13 +242,10 @@ export class MatrixRTCSession extends TypedEventEmitter { From adeb5673ec0f2ab1be4e8613c5ee1607ed52312d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 7 Sep 2023 17:13:40 +0200 Subject: [PATCH 071/113] Make `updateEncryptionKeyEvent()` public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 173fd683535..0672e9133aa 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -225,7 +225,7 @@ export class MatrixRTCSession extends TypedEventEmitter { + public async updateEncryptionKeyEvent(): Promise { if (!this.isJoined()) return; if (!this.encryptMedia) return; From e44674b8f823e470bf8edccc236d984e49d8d933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 7 Sep 2023 17:31:52 +0200 Subject: [PATCH 072/113] Only update keys based on others MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 0672e9133aa..28281e696c5 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -195,6 +195,7 @@ export class MatrixRTCSession extends TypedEventEmitter + m.member.userId === this.client.getUserId() && m.deviceId === this.client.getDeviceId(); const callMembersChanged = - oldMemberships.map(membershipToUserAndDeviceId).sort().join() !== - this.memberships.map(membershipToUserAndDeviceId).sort().join(); + oldMemberships + .filter((m) => !isMyMembership(m)) + .map(membershipToUserAndDeviceId) + .sort() + .join() !== + this.memberships + .filter((m) => !isMyMembership(m)) + .map(membershipToUserAndDeviceId) + .sort() + .join(); if (callMembersChanged && this.isJoined()) { this.updateEncryptionKeyEvent(); From f93f2f85649f4609cb5aeac476f6c5419447c674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 7 Sep 2023 17:39:02 +0200 Subject: [PATCH 073/113] Fix call order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 28281e696c5..b3e004f3631 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -195,11 +195,11 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 7 Sep 2023 17:45:08 +0200 Subject: [PATCH 074/113] Improve logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index b3e004f3631..0b096384c10 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -194,7 +194,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 7 Sep 2023 18:06:47 +0200 Subject: [PATCH 075/113] Avoid races MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 0b096384c10..76ac679440e 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -188,7 +188,7 @@ export class MatrixRTCSession extends TypedEventEmitter { if (this.isJoined()) { logger.info(`Already joined to session in room ${this.room.roomId}: ignoring join call`); return; @@ -199,8 +199,8 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 7 Sep 2023 18:27:59 +0200 Subject: [PATCH 076/113] Revert "Avoid races" This reverts commit f65ed72d6eaf71711a61db7f05e04899fb137e2d. --- src/matrixrtc/MatrixRTCSession.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 76ac679440e..0b096384c10 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -188,7 +188,7 @@ export class MatrixRTCSession extends TypedEventEmitter { + public joinRoomSession(activeFoci: Focus[], encryptMedia?: boolean): void { if (this.isJoined()) { logger.info(`Already joined to session in room ${this.room.roomId}: ignoring join call`); return; @@ -199,8 +199,8 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 7 Sep 2023 18:28:50 +0200 Subject: [PATCH 077/113] Add try-catch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 0b096384c10..f69052713ce 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -237,11 +237,15 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 7 Sep 2023 19:03:27 +0200 Subject: [PATCH 078/113] Make `updateEncryptionKeyEvent()` private MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index f69052713ce..8927909ab09 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -226,7 +226,7 @@ export class MatrixRTCSession extends TypedEventEmitter { + private async updateEncryptionKeyEvent(): Promise { if (!this.isJoined()) return; if (!this.encryptMedia) return; From 72093c8cb020a52ba67c97137e687ae65ed5c9b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 10 Sep 2023 12:10:00 +0200 Subject: [PATCH 079/113] Handle indices and throttling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSession.ts | 87 ++++++++++++++++++++++++------- src/matrixrtc/types.ts | 1 + 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 8927909ab09..6c6dc0ccb42 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -28,6 +28,7 @@ import { EncryptionKeyEventContent } from "./types"; const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000; const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event const CALL_MEMBER_EVENT_RETRY_DELAY_MIN = 3000; +const UPDATE_ENCRYPTION_KEY_THROTTLE = 3000; const getNewEncryptionKey = (): string => { const key = new Uint8Array(32); @@ -35,8 +36,8 @@ const getNewEncryptionKey = (): string => { return key.toString(); }; -const combineUserAndDeviceId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`; -const membershipToUserAndDeviceId = (m: CallMembership): string => combineUserAndDeviceId(m.member.userId, m.deviceId); +const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`; +const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.member.userId, m.deviceId); export enum MatrixRTCSessionEvent { // A member joined, left, or updated a proprty of their membership @@ -53,7 +54,11 @@ export type MatrixRTCSessionEventHandlerMap = { newMemberships: CallMembership[], ) => void; [MatrixRTCSessionEvent.JoinStateChanged]: (isJoined: boolean) => void; - [MatrixRTCSessionEvent.EncryptionKeyChanged]: (key: string, participantId: string) => void; + [MatrixRTCSessionEvent.EncryptionKeyChanged]: ( + key: string, + encryptionKeyIndex: number, + participantId: string, + ) => void; }; /** @@ -71,25 +76,44 @@ export class MatrixRTCSession extends TypedEventEmitter(); + private encryptionKeys = new Map>(); + private lastEncryptionKeyUpdateRequest?: number; + + private getNewEncryptionKeyIndex(): number { + const userId = this.client.getUserId(); + const deviceId = this.client.getDeviceId(); + + if (!userId) throw new Error("No userId!"); + if (!deviceId) throw new Error("No deviceId!"); + + return (this.getKeyForParticipant(userId, deviceId)?.length ?? 0) % 16; + } + + private setEncryptionKey( + userId: string, + deviceId: string, + encryptionKeyIndex: number, + encryptionKey: string, + ): void { + const participantId = getParticipantId(userId, deviceId); + const encryptionKeys = this.encryptionKeys.get(participantId) ?? []; - private setEncryptionKey(userId: string, deviceId: string, encryptionKey: string): void { - const participantId = combineUserAndDeviceId(userId, deviceId); - if (this.encryptionKeys.get(participantId) === encryptionKey) return; + if (encryptionKeys[encryptionKeyIndex] === encryptionKey) return; - this.encryptionKeys.set(participantId, encryptionKey); - this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKey, participantId); + encryptionKeys[encryptionKeyIndex] = encryptionKey; + this.encryptionKeys.set(participantId, encryptionKeys); + this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, encryptionKey, encryptionKeyIndex, participantId); } - public getKeyForParticipant(userId: string, deviceId: string): string | undefined { - return this.encryptionKeys.get(combineUserAndDeviceId(userId, deviceId)); + public getKeyForParticipant(userId: string, deviceId: string): Array | undefined { + return this.encryptionKeys.get(getParticipantId(userId, deviceId)); } /** * A map of keys used to encrypt and decrypt (we are using a symmetric * cipher) given participant's media. This also includes our own key */ - public getEncryptionKeys(): IterableIterator<[string, string]> { + public getEncryptionKeys(): IterableIterator<[string, Array]> { return this.encryptionKeys.entries(); } @@ -227,6 +251,19 @@ export class MatrixRTCSession extends TypedEventEmitter { + if ( + this.lastEncryptionKeyUpdateRequest && + this.lastEncryptionKeyUpdateRequest + UPDATE_ENCRYPTION_KEY_THROTTLE > Date.now() + ) { + this.lastEncryptionKeyUpdateRequest = Date.now(); + setTimeout(() => { + this.updateEncryptionKeyEvent(); + }, UPDATE_ENCRYPTION_KEY_THROTTLE); + return; + } + + this.lastEncryptionKeyUpdateRequest = Date.now(); + if (!this.isJoined()) return; if (!this.encryptMedia) return; @@ -237,9 +274,11 @@ export class MatrixRTCSession extends TypedEventEmitter(); const encryptionKey = content["m.encryption_key"]; + const encryptionKeyIndex = content["m.encryption_key_index"]; const deviceId = content["m.device_id"]; const callId = content["m.call_id"]; @@ -290,14 +331,17 @@ export class MatrixRTCSession extends TypedEventEmitter { @@ -331,12 +378,12 @@ export class MatrixRTCSession extends TypedEventEmitter !isMyMembership(m)) - .map(membershipToUserAndDeviceId) + .map(getParticipantIdFromMembership) .sort() .join() !== this.memberships .filter((m) => !isMyMembership(m)) - .map(membershipToUserAndDeviceId) + .map(getParticipantIdFromMembership) .sort() .join(); diff --git a/src/matrixrtc/types.ts b/src/matrixrtc/types.ts index 68033ab444a..c13fa8289a0 100644 --- a/src/matrixrtc/types.ts +++ b/src/matrixrtc/types.ts @@ -16,6 +16,7 @@ limitations under the License. export interface EncryptionKeyEventContent { "m.encryption_key": string; + "m.encryption_key_index": number; "m.device_id": string; "m.call_id": string; } From e288a4e9fa0fb4ef606d9214c9de1d10fc64a021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 12 Sep 2023 18:16:48 +0200 Subject: [PATCH 080/113] Fix merge mistakes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 1 + src/client.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index f65878ae53b..9572e1f8bf2 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -241,6 +241,7 @@ describe("MatrixRTCSession", () => { it("renews membership event before expiry time", async () => { jest.useFakeTimers(); let resolveFn: ((_roomId: string, _type: string, val: Record) => void) | undefined; + const eventSentPromise = new Promise>((r) => { resolveFn = (_roomId: string, _type: string, val: Record) => { r(val); diff --git a/src/client.ts b/src/client.ts index ccd7c523255..a3376ba2ab3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -219,7 +219,7 @@ import { ServerSideSecretStorageImpl, } from "./secret-storage"; import { RegisterRequest, RegisterResponse } from "./@types/registration"; -import { MatrixRTCSessionManager } from "./matrixrtc/MatrixRTCSessonManager"; +import { MatrixRTCSessionManager } from "./matrixrtc/MatrixRTCSessionManager"; export type Store = IStore; From 0b72baada1dc9c14fb4ebe1b0595cef0acf4f2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 12 Sep 2023 18:21:44 +0200 Subject: [PATCH 081/113] Mort post-merge fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/matrixrtc/MatrixRTCSessionManager.ts | 17 ++- src/matrixrtc/MatrixRTCSessonManager.ts | 145 ----------------------- 2 files changed, 16 insertions(+), 146 deletions(-) delete mode 100644 src/matrixrtc/MatrixRTCSessonManager.ts diff --git a/src/matrixrtc/MatrixRTCSessionManager.ts b/src/matrixrtc/MatrixRTCSessionManager.ts index 6f643a26416..ba520581188 100644 --- a/src/matrixrtc/MatrixRTCSessionManager.ts +++ b/src/matrixrtc/MatrixRTCSessionManager.ts @@ -17,10 +17,11 @@ limitations under the License. import { logger } from "../logger"; import { MatrixClient, ClientEvent } from "../client"; import { TypedEventEmitter } from "../models/typed-event-emitter"; -import { Room } from "../models/room"; +import { Room, RoomEvent } from "../models/room"; import { RoomState, RoomStateEvent } from "../models/room-state"; import { MatrixEvent } from "../models/event"; import { MatrixRTCSession } from "./MatrixRTCSession"; +import { EventType } from "../@types/event"; export enum MatrixRTCSessionManagerEvents { // A member has joined the MatrixRTC session, creating an active session in a room where there wasn't previously @@ -62,6 +63,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter { + if (event.getType() !== EventType.CallEncryptionPrefix) return; + + const room = this.client.getRoom(event.getRoomId()); + if (!room) { + logger.error(`Got room state event for unknown room ${event.getRoomId()}!`); + return; + } + + this.getRoomSession(room).onCallEncryption(event); + }; + private onRoom = (room: Room): void => { this.refreshRoom(room); }; diff --git a/src/matrixrtc/MatrixRTCSessonManager.ts b/src/matrixrtc/MatrixRTCSessonManager.ts deleted file mode 100644 index 71ca5495b86..00000000000 --- a/src/matrixrtc/MatrixRTCSessonManager.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* -Copyright 2023 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { logger } from "../logger"; -import { MatrixClient, ClientEvent } from "../client"; -import { TypedEventEmitter } from "../models/typed-event-emitter"; -import { Room, RoomEvent } from "../models/room"; -import { RoomState, RoomStateEvent } from "../models/room-state"; -import { MatrixEvent } from "../models/event"; -import { MatrixRTCSession } from "./MatrixRTCSession"; -import { EventType } from "../@types/event"; - -export enum MatrixRTCSessionManagerEvents { - // A member has joined the MatrixRTC session, creating an active session in a room where there wasn't previously - SessionStarted = "session_started", - // All participants have left a given MatrixRTC session. - SessionEnded = "session_ended", -} - -type EventHandlerMap = { - [MatrixRTCSessionManagerEvents.SessionStarted]: (roomId: string, session: MatrixRTCSession) => void; - [MatrixRTCSessionManagerEvents.SessionEnded]: (roomId: string, session: MatrixRTCSession) => void; -}; - -export class MatrixRTCSessionManager extends TypedEventEmitter { - // All the room-scoped sessions we know about. This will include any where the app - // has queried for the MatrixRTC sessions in a room, whether it's ever had any members - // or not) - private roomSessions = new Map(); - - public constructor(private client: MatrixClient) { - super(); - } - - public start(): void { - // We shouldn't need to null-check here, but matrix-client.spec.ts mocks getRooms - // returing nothing, and breaks tests if you change it to return an empty array :'( - for (const room of this.client.getRooms() ?? []) { - const session = MatrixRTCSession.roomSessionForRoom(this.client, room); - if (session.memberships.length > 0) { - this.roomSessions.set(room.roomId, session); - } - } - - this.client.on(ClientEvent.Room, this.onRoom); - this.client.on(RoomEvent.Timeline, this.onTimeline); - this.client.on(RoomStateEvent.Events, this.onRoomState); - } - - public stop(): void { - for (const sess of this.roomSessions.values()) { - sess.stop(); - } - this.roomSessions.clear(); - - this.client.removeListener(ClientEvent.Room, this.onRoom); - this.client.removeListener(RoomEvent.Timeline, this.onTimeline); - this.client.removeListener(RoomStateEvent.Events, this.onRoomState); - } - - /** - * Get a list of all ongoing MatrixRTC sessions that have 1 or more active - * members - * (whether the client is joined to them or not) - */ - public getActiveSessions(): MatrixRTCSession[] { - return Array.from(this.roomSessions.values()).filter((m) => m.memberships.length > 0); - } - - /** - * Gets the main MatrixRTC session for a room, or undefined if there is - * no current session - */ - public getActiveRoomSession(room: Room): MatrixRTCSession | undefined { - return this.roomSessions.get(room.roomId)!; - } - - /** - * Gets the main MatrixRTC session for a room, returning an empty session - * if no members are currently participating - */ - public getRoomSession(room: Room): MatrixRTCSession { - if (!this.roomSessions.has(room.roomId)) { - this.roomSessions.set(room.roomId, MatrixRTCSession.roomSessionForRoom(this.client, room)); - } - - return this.roomSessions.get(room.roomId)!; - } - - private onTimeline = (event: MatrixEvent): void => { - if (event.getType() !== EventType.CallEncryptionPrefix) return; - - const room = this.client.getRoom(event.getRoomId()); - if (!room) { - logger.error(`Got room state event for unknown room ${event.getRoomId()}!`); - return; - } - - this.getRoomSession(room).onCallEncryption(event); - }; - - private onRoom = (room: Room): void => { - this.refreshRoom(room); - }; - - private onRoomState = (event: MatrixEvent, _state: RoomState): void => { - const room = this.client.getRoom(event.getRoomId()); - if (!room) { - logger.error(`Got room state event for unknown room ${event.getRoomId()}!`); - return; - } - - this.refreshRoom(room); - }; - - private refreshRoom(room: Room): void { - const hadSession = this.roomSessions.has(room.roomId); - const sess = this.getRoomSession(room); - - const wasActive = sess.memberships.length > 0 && hadSession; - - sess.onMembershipUpdate(); - - const nowActive = sess.memberships.length > 0; - - if (wasActive && !nowActive) { - this.emit(MatrixRTCSessionManagerEvents.SessionEnded, room.roomId, this.roomSessions.get(room.roomId)!); - } else if (!wasActive && nowActive) { - this.emit(MatrixRTCSessionManagerEvents.SessionStarted, room.roomId, this.roomSessions.get(room.roomId)!); - } - } -} From df62adcec665e7ff3b602dffa72d7e175b38b9d3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Oct 2023 15:17:40 +0100 Subject: [PATCH 082/113] Split out key generation from key sending And send all keys in a key event (changes the format of the key event) rather than just the one we just generated. --- src/@types/event.ts | 2 +- src/matrixrtc/MatrixRTCSession.ts | 101 +++++++++++++++-------- src/matrixrtc/MatrixRTCSessionManager.ts | 2 +- src/matrixrtc/types.ts | 10 ++- 4 files changed, 74 insertions(+), 41 deletions(-) diff --git a/src/@types/event.ts b/src/@types/event.ts index 1e05bec37e0..2111e3988bd 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -55,7 +55,7 @@ export enum EventType { CallReplaces = "m.call.replaces", CallAssertedIdentity = "m.call.asserted_identity", CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity", - CallEncryptionPrefix = "io.element.call.encryption_key", + CallEncryptionKeysPrefix = "io.element.call.encryption_keys", KeyVerificationRequest = "m.key.verification.request", KeyVerificationStart = "m.key.verification.start", KeyVerificationCancel = "m.key.verification.cancel", diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index ffed665b815..511e3424756 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -23,8 +23,8 @@ import { EventType } from "../@types/event"; import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; import { MatrixEvent } from "../matrix"; -import { EncryptionKeyEventContent } from "./types"; import { randomString } from "../randomstring"; +import { EncryptionKeysEventContent } from "./types"; const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000; const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event @@ -100,7 +100,7 @@ export class MatrixRTCSession extends TypedEventEmitter | undefined { + public getKeysForParticipant(userId: string, deviceId: string): Array | undefined { return this.encryptionKeys.get(getParticipantId(userId, deviceId)); } @@ -246,6 +246,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date.now() ) { + logger.info("Last encryption key event sent too recently: postponing"); this.lastEncryptionKeyUpdateRequest = Date.now(); setTimeout(() => { this.updateEncryptionKeyEvent(); @@ -315,24 +329,32 @@ export class MatrixRTCSession extends TypedEventEmitter { + return { + index, + key, + }; + }), "m.device_id": deviceId, "m.call_id": "", - } as EncryptionKeyEventContent); + } as EncryptionKeysEventContent); } catch (error) { logger.error("Failed to send m.call.encryption_key", error); } logger.debug( - `Embedded-E2EE-LOG updateEncryptionKeyEvent participantId=${userId}:${deviceId} encryptionKeyIndex=${encryptionKeyIndex} encryptionKey=${encryptionKey}`, + `Embedded-E2EE-LOG updateEncryptionKeyEvent participantId=${userId}:${deviceId} numSent=${myKeys.length}`, this.encryptionKeys, ); - this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey); } /** @@ -363,43 +385,50 @@ export class MatrixRTCSession extends TypedEventEmitter { const userId = event.getSender(); - const content = event.getContent(); - const encryptionKey = content["m.encryption_key"]; - const encryptionKeyIndex = content["m.encryption_key_index"]; + const content = event.getContent(); + const deviceId = content["m.device_id"]; const callId = content["m.call_id"]; - if ( - !userId || - !deviceId || - !encryptionKey || - encryptionKeyIndex === undefined || - encryptionKeyIndex === null || - callId === undefined || - callId === null || - typeof deviceId !== "string" || - typeof callId !== "string" || - typeof encryptionKey !== "string" || - typeof encryptionKeyIndex !== "number" - ) { - throw new Error( - `Malformed m.call.encryption_key: userId=${userId}, deviceId=${deviceId}, encryptionKeyIndex=${encryptionKeyIndex} callId=${callId}`, - ); + if (!userId) { + logger.warn(`Received m.call.encryption_keys with no userId: callId=${callId}`); + return; } // We currently only handle callId = "" if (callId !== "") { logger.warn( - `Received m.call.encryption_key with unsupported callId: userId=${userId}, deviceId=${deviceId}, callId=${callId}`, + `Received m.call.encryption_keys with unsupported callId: userId=${userId}, deviceId=${deviceId}, callId=${callId}`, ); return; } - logger.debug( - `Embedded-E2EE-LOG onCallEncryption userId=${userId}:${deviceId} encryptionKey=${encryptionKey} encryptionKeyIndex=${encryptionKeyIndex}`, - this.encryptionKeys, - ); - this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey); + for (const key of content.keys) { + const encryptionKey = key.key; + const encryptionKeyIndex = key.index; + + if ( + !encryptionKey || + encryptionKeyIndex === undefined || + encryptionKeyIndex === null || + callId === undefined || + callId === null || + typeof deviceId !== "string" || + typeof callId !== "string" || + typeof encryptionKey !== "string" || + typeof encryptionKeyIndex !== "number" + ) { + logger.warn( + `Malformed call encryption_key: userId=${userId}, deviceId=${deviceId}, encryptionKeyIndex=${encryptionKeyIndex} callId=${callId}`, + ); + } else { + logger.debug( + `Embedded-E2EE-LOG onCallEncryption userId=${userId}:${deviceId} encryptionKey=${encryptionKey} encryptionKeyIndex=${encryptionKeyIndex}`, + this.encryptionKeys, + ); + this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey); + } + } }; public onMembershipUpdate = (): void => { diff --git a/src/matrixrtc/MatrixRTCSessionManager.ts b/src/matrixrtc/MatrixRTCSessionManager.ts index ba520581188..ba1eb0fa1dd 100644 --- a/src/matrixrtc/MatrixRTCSessionManager.ts +++ b/src/matrixrtc/MatrixRTCSessionManager.ts @@ -99,7 +99,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter { - if (event.getType() !== EventType.CallEncryptionPrefix) return; + if (event.getType() !== EventType.CallEncryptionKeysPrefix) return; const room = this.client.getRoom(event.getRoomId()); if (!room) { diff --git a/src/matrixrtc/types.ts b/src/matrixrtc/types.ts index c13fa8289a0..0cdbf70e513 100644 --- a/src/matrixrtc/types.ts +++ b/src/matrixrtc/types.ts @@ -14,9 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -export interface EncryptionKeyEventContent { - "m.encryption_key": string; - "m.encryption_key_index": number; +export interface EncryptionKeyEntry { + index: number; + key: string; +} + +export interface EncryptionKeysEventContent { + "keys": EncryptionKeyEntry[]; "m.device_id": string; "m.call_id": string; } From 2b6135232243b8c5c0fbe4b7441a69b07216ea22 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Oct 2023 12:06:54 +0100 Subject: [PATCH 083/113] Remember and clear the timeout for the send key event So we don't schedule more key updates if one is already pending. Also don't update the last sent time when we didn't actually send the keys. --- src/matrixrtc/MatrixRTCSession.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index edf82decbf4..227eabbf02e 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -83,6 +83,7 @@ export class MatrixRTCSession extends TypedEventEmitter; private expiryTimeout?: ReturnType; + private keysEventUpdateTimeout?: ReturnType; private activeFoci: Focus[] | undefined; @@ -305,19 +306,22 @@ export class MatrixRTCSession extends TypedEventEmitter { + private updateEncryptionKeyEvent = async (): Promise => { if ( this.lastEncryptionKeyUpdateRequest && this.lastEncryptionKeyUpdateRequest + UPDATE_ENCRYPTION_KEY_THROTTLE > Date.now() ) { logger.info("Last encryption key event sent too recently: postponing"); - this.lastEncryptionKeyUpdateRequest = Date.now(); - setTimeout(() => { - this.updateEncryptionKeyEvent(); - }, UPDATE_ENCRYPTION_KEY_THROTTLE); + if (this.keysEventUpdateTimeout === undefined) { + this.keysEventUpdateTimeout = setTimeout(this.updateEncryptionKeyEvent, UPDATE_ENCRYPTION_KEY_THROTTLE); + } return; } + if (this.keysEventUpdateTimeout !== undefined) { + clearTimeout(this.keysEventUpdateTimeout); + this.keysEventUpdateTimeout = undefined; + } this.lastEncryptionKeyUpdateRequest = Date.now(); if (!this.isJoined()) return; @@ -355,7 +359,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 19 Oct 2023 14:39:52 +0100 Subject: [PATCH 084/113] Make key event resends more robust --- src/matrixrtc/MatrixRTCSession.ts | 57 ++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 227eabbf02e..f4e80256c5d 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -22,7 +22,7 @@ import { MatrixClient } from "../client"; import { EventType } from "../@types/event"; import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; -import { MatrixEvent } from "../matrix"; +import { MatrixError, MatrixEvent } from "../matrix"; import { randomString } from "../randomstring"; import { EncryptionKeysEventContent } from "./types"; @@ -248,7 +248,7 @@ export class MatrixRTCSession extends TypedEventEmitter resolve(false)); } + const userId = this.client.getUserId(); + const deviceId = this.client.getDeviceId(); + + if (!userId) throw new Error("No userId"); + if (!deviceId) throw new Error("No deviceId"); + + // clear our encryption keys as we're done with them now (we'll + // make new keys if we rejoin). We leave keys for other participants + // as they may still be using the same ones. + this.encryptionKeys.set(getParticipantId(userId, deviceId), []); + logger.info(`Leaving call session in room ${this.room.roomId}`); this.relativeExpiry = undefined; this.activeFoci = undefined; @@ -304,26 +315,36 @@ export class MatrixRTCSession extends TypedEventEmitter => { + private requestKeyEventSend(): void { if ( this.lastEncryptionKeyUpdateRequest && this.lastEncryptionKeyUpdateRequest + UPDATE_ENCRYPTION_KEY_THROTTLE > Date.now() ) { logger.info("Last encryption key event sent too recently: postponing"); if (this.keysEventUpdateTimeout === undefined) { - this.keysEventUpdateTimeout = setTimeout(this.updateEncryptionKeyEvent, UPDATE_ENCRYPTION_KEY_THROTTLE); + this.keysEventUpdateTimeout = setTimeout(this.sendEncryptionKeysEvent, UPDATE_ENCRYPTION_KEY_THROTTLE); } return; } + this.sendEncryptionKeysEvent(); + } + + /** + * Re-sends the encryption keys room event + */ + private sendEncryptionKeysEvent = async (): Promise => { if (this.keysEventUpdateTimeout !== undefined) { clearTimeout(this.keysEventUpdateTimeout); this.keysEventUpdateTimeout = undefined; } this.lastEncryptionKeyUpdateRequest = Date.now(); + logger.info("Sending encryption keys event"); + if (!this.isJoined()) return; if (!this.encryptMedia) return; @@ -351,14 +372,26 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Thu, 19 Oct 2023 14:53:58 +0100 Subject: [PATCH 085/113] Attempt to make tests pass --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index f8c229c9a7f..85b2273de08 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import crypto from "crypto"; + import { EventTimeline, EventType, MatrixClient, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; @@ -29,6 +31,8 @@ const membershipTemplate: CallMembershipData = { membershipID: "bloop", }; +global.crypto.getRandomValues = crypto.randomBytes; + const mockFocus = { type: "mock" }; describe("MatrixRTCSession", () => { From 19db44f1f763b97e3bd388fba7c356dfb731758a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Oct 2023 15:03:59 +0100 Subject: [PATCH 086/113] crypto wasn't defined at all --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 85b2273de08..4284756e71f 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -31,7 +31,10 @@ const membershipTemplate: CallMembershipData = { membershipID: "bloop", }; -global.crypto.getRandomValues = crypto.randomBytes; +if (!global.crypto) { + // @ts-ignore + global.crypto = { getRandomValues: crypto.randomBytes }; +} const mockFocus = { type: "mock" }; From a8ea202bc349fe766535770d708d1f21ced671ec Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Oct 2023 15:13:01 +0100 Subject: [PATCH 087/113] Hopefully get interface right --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 4284756e71f..39046f1c75e 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -33,7 +33,7 @@ const membershipTemplate: CallMembershipData = { if (!global.crypto) { // @ts-ignore - global.crypto = { getRandomValues: crypto.randomBytes }; + global.crypto = { getRandomValues: (array) => crypto.randomBytes(array.length) }; } const mockFocus = { type: "mock" }; From 4120641b3d3983733044967812e475bc3610d1e7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 20 Oct 2023 17:29:39 +0100 Subject: [PATCH 088/113] Fix key format on the wire to base64 --- src/base64.ts | 11 ++++++++++- src/matrixrtc/MatrixRTCSession.ts | 32 +++++++++++++++---------------- src/randomstring.ts | 9 +++++++++ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/base64.ts b/src/base64.ts index 5a4c5c87a06..79bc5a49380 100644 --- a/src/base64.ts +++ b/src/base64.ts @@ -54,7 +54,16 @@ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): stri } /** - * Decode a base64 string to a typed array of uint8. + * Encode a typed array of uint8 as unpadded base64 using the URL-safe encoding. + * @param uint8Array - The data to encode. + * @returns The unpadded base64. + */ +export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): string { + return encodeUnpaddedBase64(uint8Array).replace("+", "-").replace("/", "_"); +} + +/** + * Decode a base64 (or base64url) string to a typed array of uint8. * @param base64 - The base64 to decode. * @returns The decoded data. */ diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 74133539411..e679b2559b3 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -23,20 +23,15 @@ import { EventType } from "../@types/event"; import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; import { MatrixError, MatrixEvent } from "../matrix"; -import { randomString } from "../randomstring"; +import { randomString, secureRandomBase64 } from "../randomstring"; import { EncryptionKeysEventContent } from "./types"; +import { decodeBase64, encodeUnpaddedBase64Url } from "../base64"; const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000; const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event const CALL_MEMBER_EVENT_RETRY_DELAY_MIN = 3000; const UPDATE_ENCRYPTION_KEY_THROTTLE = 3000; -const getNewEncryptionKey = (): string => { - const key = new Uint8Array(32); - crypto.getRandomValues(key); - return key.toString(); -}; - const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`; const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId); @@ -58,7 +53,7 @@ export type MatrixRTCSessionEventHandlerMap = { ) => void; [MatrixRTCSessionEvent.JoinStateChanged]: (isJoined: boolean) => void; [MatrixRTCSessionEvent.EncryptionKeyChanged]: ( - key: string, + key: Uint8Array, encryptionKeyIndex: number, participantId: string, ) => void; @@ -91,7 +86,8 @@ export class MatrixRTCSession extends TypedEventEmitter>(); + // userId:deviceId => array of keys + private encryptionKeys = new Map>(); private lastEncryptionKeyUpdateRequest?: number; private getNewEncryptionKeyIndex(): number { @@ -108,19 +104,21 @@ export class MatrixRTCSession extends TypedEventEmitter | undefined { + public getKeysForParticipant(userId: string, deviceId: string): Array | undefined { return this.encryptionKeys.get(getParticipantId(userId, deviceId)); } @@ -128,7 +126,7 @@ export class MatrixRTCSession extends TypedEventEmitter]> { + public getEncryptionKeys(): IterableIterator<[string, Array]> { return this.encryptionKeys.entries(); } @@ -311,7 +309,7 @@ export class MatrixRTCSession extends TypedEventEmitter { return { index, - key, + key: encodeUnpaddedBase64Url(key), }; }), "m.device_id": deviceId, diff --git a/src/randomstring.ts b/src/randomstring.ts index 0ed46fb3895..7b597319594 100644 --- a/src/randomstring.ts +++ b/src/randomstring.ts @@ -15,10 +15,19 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { encodeUnpaddedBase64Url } from "./base64"; + const LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const DIGITS = "0123456789"; +export function secureRandomBase64(len: number): string { + const key = new Uint8Array(len); + crypto.getRandomValues(key); + // encode to base64url as this value goes into URLs + return encodeUnpaddedBase64Url(key); +} + export function randomString(len: number): string { return randomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS); } From 5dd37f11eb8e06f9e7f3c10235db2db0cb2ea7d7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Oct 2023 11:53:05 +0100 Subject: [PATCH 089/113] Add comment --- src/matrixrtc/MatrixRTCSession.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index e679b2559b3..3027ad9043d 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -302,6 +302,9 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 23 Oct 2023 12:19:39 +0100 Subject: [PATCH 090/113] More standard method order --- src/matrixrtc/MatrixRTCSession.ts | 80 +++++++++++++++---------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 3027ad9043d..001b825cde3 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -90,46 +90,6 @@ export class MatrixRTCSession extends TypedEventEmitter>(); private lastEncryptionKeyUpdateRequest?: number; - private getNewEncryptionKeyIndex(): number { - const userId = this.client.getUserId(); - const deviceId = this.client.getDeviceId(); - - if (!userId) throw new Error("No userId!"); - if (!deviceId) throw new Error("No deviceId!"); - - return (this.getKeysForParticipant(userId, deviceId)?.length ?? 0) % 16; - } - - private setEncryptionKey( - userId: string, - deviceId: string, - encryptionKeyIndex: number, - encryptionKeyString: string, - ): void { - const keyBin = decodeBase64(encryptionKeyString); - - const participantId = getParticipantId(userId, deviceId); - const encryptionKeys = this.encryptionKeys.get(participantId) ?? []; - - if (encryptionKeys[encryptionKeyIndex] === keyBin) return; - - encryptionKeys[encryptionKeyIndex] = keyBin; - this.encryptionKeys.set(participantId, encryptionKeys); - this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId); - } - - public getKeysForParticipant(userId: string, deviceId: string): Array | undefined { - return this.encryptionKeys.get(getParticipantId(userId, deviceId)); - } - - /** - * A map of keys used to encrypt and decrypt (we are using a symmetric - * cipher) given participant's media. This also includes our own key - */ - public getEncryptionKeys(): IterableIterator<[string, Array]> { - return this.encryptionKeys.entries(); - } - /** * Returns all the call memberships for a room, oldest first */ @@ -302,6 +262,46 @@ export class MatrixRTCSession extends TypedEventEmitter | undefined { + return this.encryptionKeys.get(getParticipantId(userId, deviceId)); + } + + /** + * A map of keys used to encrypt and decrypt (we are using a symmetric + * cipher) given participant's media. This also includes our own key + */ + public getEncryptionKeys(): IterableIterator<[string, Array]> { + return this.encryptionKeys.entries(); + } + + private getNewEncryptionKeyIndex(): number { + const userId = this.client.getUserId(); + const deviceId = this.client.getDeviceId(); + + if (!userId) throw new Error("No userId!"); + if (!deviceId) throw new Error("No deviceId!"); + + return (this.getKeysForParticipant(userId, deviceId)?.length ?? 0) % 16; + } + + private setEncryptionKey( + userId: string, + deviceId: string, + encryptionKeyIndex: number, + encryptionKeyString: string, + ): void { + const keyBin = decodeBase64(encryptionKeyString); + + const participantId = getParticipantId(userId, deviceId); + const encryptionKeys = this.encryptionKeys.get(participantId) ?? []; + + if (encryptionKeys[encryptionKeyIndex] === keyBin) return; + + encryptionKeys[encryptionKeyIndex] = keyBin; + this.encryptionKeys.set(participantId, encryptionKeys); + this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId); + } + /** * Generate a new sender key and add it at the next available index */ From 0f03cab9e2eb6305032c1a78c2ae2a7b9c0cc658 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Oct 2023 12:25:57 +0100 Subject: [PATCH 091/113] Rename encryptMedia The js-sdk doesn't do media and therefore doesn't do media encryption --- src/matrixrtc/MatrixRTCSession.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 001b825cde3..cad96218c62 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -85,7 +85,7 @@ export class MatrixRTCSession extends TypedEventEmitter array of keys private encryptionKeys = new Map>(); private lastEncryptionKeyUpdateRequest?: number; @@ -194,21 +194,28 @@ export class MatrixRTCSession extends TypedEventEmitter Date.now() @@ -349,7 +358,6 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 23 Oct 2023 12:30:13 +0100 Subject: [PATCH 092/113] Stop logging encryption keys now --- src/matrixrtc/MatrixRTCSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index cad96218c62..621bee8d823 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -471,7 +471,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 23 Oct 2023 13:29:21 +0100 Subject: [PATCH 093/113] Use regular base64 It's not going in a URL, so no need --- src/base64.ts | 9 --------- src/matrixrtc/MatrixRTCSession.ts | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/base64.ts b/src/base64.ts index 79bc5a49380..115a5014a90 100644 --- a/src/base64.ts +++ b/src/base64.ts @@ -53,15 +53,6 @@ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): stri return encodeBase64(uint8Array).replace(/={1,2}$/, ""); } -/** - * Encode a typed array of uint8 as unpadded base64 using the URL-safe encoding. - * @param uint8Array - The data to encode. - * @returns The unpadded base64. - */ -export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): string { - return encodeUnpaddedBase64(uint8Array).replace("+", "-").replace("/", "_"); -} - /** * Decode a base64 (or base64url) string to a typed array of uint8. * @param base64 - The base64 to decode. diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 621bee8d823..2a9d825a202 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -25,7 +25,7 @@ import { Focus } from "./focus"; import { MatrixError, MatrixEvent } from "../matrix"; import { randomString, secureRandomBase64 } from "../randomstring"; import { EncryptionKeysEventContent } from "./types"; -import { decodeBase64, encodeUnpaddedBase64Url } from "../base64"; +import { decodeBase64, encodeUnpaddedBase64 } from "../base64"; const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000; const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event @@ -377,7 +377,7 @@ export class MatrixRTCSession extends TypedEventEmitter { return { index, - key: encodeUnpaddedBase64Url(key), + key: encodeUnpaddedBase64(key), }; }), "m.device_id": deviceId, From fe59d2a341ea1a60c469a894892195ddb75c9265 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Oct 2023 13:33:04 +0100 Subject: [PATCH 094/113] Re-add base64url randomstring was using it. Also give it a test. --- spec/unit/base64.spec.ts | 18 +++++++++++++----- src/base64.ts | 9 +++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/spec/unit/base64.spec.ts b/spec/unit/base64.spec.ts index 0639f785ac4..4646fbd84a5 100644 --- a/spec/unit/base64.spec.ts +++ b/spec/unit/base64.spec.ts @@ -17,7 +17,7 @@ limitations under the License. import { TextEncoder, TextDecoder } from "util"; import NodeBuffer from "node:buffer"; -import { decodeBase64, encodeBase64, encodeUnpaddedBase64 } from "../../src/base64"; +import { decodeBase64, encodeBase64, encodeUnpaddedBase64, encodeUnpaddedBase64Url } from "../../src/base64"; describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => { let origBuffer = Buffer; @@ -43,19 +43,27 @@ describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => { global.btoa = undefined; }); - it("Should decode properly encoded data", async () => { + it("Should decode properly encoded data", () => { const decoded = new TextDecoder().decode(decodeBase64("ZW5jb2RpbmcgaGVsbG8gd29ybGQ=")); expect(decoded).toStrictEqual("encoding hello world"); }); - it("Should decode URL-safe base64", async () => { + it("Should encode unpadded URL-safe base64", () => { + const toEncode = "?????"; + const data = new TextEncoder().encode(toEncode); + + const encoded = encodeUnpaddedBase64Url(data); + expect(encoded).toEqual("Pz8_Pz8"); + }); + + it("Should decode URL-safe base64", () => { const decoded = new TextDecoder().decode(decodeBase64("Pz8_Pz8=")); expect(decoded).toStrictEqual("?????"); }); - it("Encode unpadded should not have padding", async () => { + it("Encode unpadded should not have padding", () => { const toEncode = "encoding hello world"; const data = new TextEncoder().encode(toEncode); @@ -68,7 +76,7 @@ describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => { expect(padding).toStrictEqual("="); }); - it("Decode should be indifferent to padding", async () => { + it("Decode should be indifferent to padding", () => { const withPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ="; const withoutPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ"; diff --git a/src/base64.ts b/src/base64.ts index 115a5014a90..79bc5a49380 100644 --- a/src/base64.ts +++ b/src/base64.ts @@ -53,6 +53,15 @@ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): stri return encodeBase64(uint8Array).replace(/={1,2}$/, ""); } +/** + * Encode a typed array of uint8 as unpadded base64 using the URL-safe encoding. + * @param uint8Array - The data to encode. + * @returns The unpadded base64. + */ +export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): string { + return encodeUnpaddedBase64(uint8Array).replace("+", "-").replace("/", "_"); +} + /** * Decode a base64 (or base64url) string to a typed array of uint8. * @param base64 - The base64 to decode. From ab86c7cb61cd7dc17b8f8f4e098ffae1d59e8078 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Oct 2023 13:53:27 +0100 Subject: [PATCH 095/113] Add tests for randomstring --- spec/unit/randomstring.spec.ts | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 spec/unit/randomstring.spec.ts diff --git a/spec/unit/randomstring.spec.ts b/spec/unit/randomstring.spec.ts new file mode 100644 index 00000000000..8194c9186a6 --- /dev/null +++ b/spec/unit/randomstring.spec.ts @@ -0,0 +1,61 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { decodeBase64 } from "../../src/base64"; +import { randomLowercaseString, randomString, randomUppercaseString, secureRandomBase64 } from "../../src/randomstring"; + +describe("Random strings", () => { + it.each([8, 16, 32])("secureRandomBase64 generates %i valid base64 bytes", (n: number) => { + const randb641 = secureRandomBase64(n); + const randb642 = secureRandomBase64(n); + + expect(randb641).not.toEqual(randb642); + + const decoded = decodeBase64(randb641); + expect(decoded).toHaveLength(n); + }); + + it.each([8, 16, 32])("randomString generates string of %i characters", (n: number) => { + const rand1 = randomString(n); + const rand2 = randomString(n); + + expect(rand1).not.toEqual(rand2); + + expect(rand1).toHaveLength(n); + }); + + it.each([8, 16, 32])("randomLowercaseString generates lowercase string of %i characters", (n: number) => { + const rand1 = randomLowercaseString(n); + const rand2 = randomLowercaseString(n); + + expect(rand1).not.toEqual(rand2); + + expect(rand1).toHaveLength(n); + + expect(rand1.toLowerCase()).toEqual(rand1); + }); + + it.each([8, 16, 32])("randomUppercaseString generates lowercase string of %i characters", (n: number) => { + const rand1 = randomUppercaseString(n); + const rand2 = randomUppercaseString(n); + + expect(rand1).not.toEqual(rand2); + + expect(rand1).toHaveLength(n); + + expect(rand1.toUpperCase()).toEqual(rand1); + }); +}); From 89dafa106da25a5f5079388d8ec467c1115b6c5d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Oct 2023 15:37:47 +0100 Subject: [PATCH 096/113] Switch between either browser or node crypto Let's see if this will work... --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 7 ------- src/randomstring.ts | 7 ++++++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 39046f1c75e..f8c229c9a7f 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import crypto from "crypto"; - import { EventTimeline, EventType, MatrixClient, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; @@ -31,11 +29,6 @@ const membershipTemplate: CallMembershipData = { membershipID: "bloop", }; -if (!global.crypto) { - // @ts-ignore - global.crypto = { getRandomValues: (array) => crypto.randomBytes(array.length) }; -} - const mockFocus = { type: "mock" }; describe("MatrixRTCSession", () => { diff --git a/src/randomstring.ts b/src/randomstring.ts index 7b597319594..63ba8b119f0 100644 --- a/src/randomstring.ts +++ b/src/randomstring.ts @@ -15,15 +15,20 @@ See the License for the specific language governing permissions and limitations under the License. */ +import nodeCrypto from "node:crypto"; + import { encodeUnpaddedBase64Url } from "./base64"; const LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const DIGITS = "0123456789"; +const webCrypto = typeof crypto === "object" ? crypto : (nodeCrypto.webcrypto as typeof crypto); + export function secureRandomBase64(len: number): string { const key = new Uint8Array(len); - crypto.getRandomValues(key); + webCrypto.getRandomValues(key); + // encode to base64url as this value goes into URLs return encodeUnpaddedBase64Url(key); } From dbad9201a955965a282650988cfb7c70d341e12a Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Oct 2023 16:29:05 +0100 Subject: [PATCH 097/113] Obviously crypto has already solved this --- src/randomstring.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/randomstring.ts b/src/randomstring.ts index 63ba8b119f0..bde1e851208 100644 --- a/src/randomstring.ts +++ b/src/randomstring.ts @@ -15,19 +15,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import nodeCrypto from "node:crypto"; - import { encodeUnpaddedBase64Url } from "./base64"; +import { crypto } from "./crypto/crypto"; const LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const DIGITS = "0123456789"; -const webCrypto = typeof crypto === "object" ? crypto : (nodeCrypto.webcrypto as typeof crypto); - export function secureRandomBase64(len: number): string { const key = new Uint8Array(len); - webCrypto.getRandomValues(key); + crypto.getRandomValues(key); // encode to base64url as this value goes into URLs return encodeUnpaddedBase64Url(key); From 6e80cf85aecc316b17b2db689188c5c426319cdc Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Oct 2023 18:11:31 +0100 Subject: [PATCH 098/113] Some tests for MatrixRTCSession key stuff --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 22 +++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index f8c229c9a7f..66664200408 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -184,8 +184,15 @@ describe("MatrixRTCSession", () => { describe("joining", () => { let mockRoom: Room; + let sendStateEventMock: jest.Mock; + let sendEventMock: jest.Mock; beforeEach(() => { + sendStateEventMock = jest.fn(); + sendEventMock = jest.fn(); + client.sendStateEvent = sendStateEventMock; + client.sendEvent = sendEventMock; + mockRoom = makeMockRoom([]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); }); @@ -205,8 +212,6 @@ describe("MatrixRTCSession", () => { }); it("sends a membership event when joining a call", () => { - client.sendStateEvent = jest.fn(); - sess!.joinRoomSession([mockFocus]); expect(client.sendStateEvent).toHaveBeenCalledWith( @@ -230,9 +235,6 @@ describe("MatrixRTCSession", () => { }); it("does nothing if join called when already joined", () => { - const sendStateEventMock = jest.fn(); - client.sendStateEvent = sendStateEventMock; - sess!.joinRoomSession([mockFocus]); expect(client.sendStateEvent).toHaveBeenCalledTimes(1); @@ -299,6 +301,16 @@ describe("MatrixRTCSession", () => { jest.useRealTimers(); } }); + + it("creates & sends a key when joining", () => { + sess!.joinRoomSession([mockFocus], true); + const keys = sess?.getKeysForParticipant("@alice:example.org", "AAAAAAA"); + expect(keys).toHaveLength(1); + + const allKeys = sess!.getEncryptionKeys(); + expect(allKeys).toBeTruthy(); + expect(Array.from(allKeys)).toHaveLength(1); + }); }); it("emits an event at the time a membership event expires", () => { From 90b5c4669a67be8bedce2fdc063fa6ddcbba3265 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Oct 2023 18:21:50 +0100 Subject: [PATCH 099/113] Test keys object contents --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 66664200408..1f242d4ec7d 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -302,7 +302,7 @@ describe("MatrixRTCSession", () => { } }); - it("creates & sends a key when joining", () => { + it("creates a key when joining", () => { sess!.joinRoomSession([mockFocus], true); const keys = sess?.getKeysForParticipant("@alice:example.org", "AAAAAAA"); expect(keys).toHaveLength(1); @@ -311,6 +311,27 @@ describe("MatrixRTCSession", () => { expect(allKeys).toBeTruthy(); expect(Array.from(allKeys)).toHaveLength(1); }); + + it("sends keys when joining", async () => { + const eventSentPromise = new Promise((resolve) => { + sendEventMock.mockImplementation(resolve); + }); + + sess!.joinRoomSession([mockFocus], true); + + await eventSentPromise; + + expect(sendEventMock).toHaveBeenCalledWith(expect.stringMatching(".*"), "io.element.call.encryption_keys", { + "m.call_id": "", + "m.device_id": "AAAAAAA", + "keys": [ + { + index: 0, + key: expect.stringMatching(".*"), + }, + ], + }); + }); }); it("emits an event at the time a membership event expires", () => { From c48f1ebe573e7321ca2f69e3af8260b2276b7f4d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Oct 2023 18:23:21 +0100 Subject: [PATCH 100/113] Change keys event format To move away from m. keys --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 6 +++--- src/matrixrtc/MatrixRTCSession.ts | 10 +++++----- src/matrixrtc/types.ts | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 1f242d4ec7d..88dc89a46f1 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -322,9 +322,9 @@ describe("MatrixRTCSession", () => { await eventSentPromise; expect(sendEventMock).toHaveBeenCalledWith(expect.stringMatching(".*"), "io.element.call.encryption_keys", { - "m.call_id": "", - "m.device_id": "AAAAAAA", - "keys": [ + call_id: "", + device_id: "AAAAAAA", + keys: [ { index: 0, key: expect.stringMatching(".*"), diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 2a9d825a202..607c68d9623 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -374,14 +374,14 @@ export class MatrixRTCSession extends TypedEventEmitter { + keys: myKeys.map((key, index) => { return { index, key: encodeUnpaddedBase64(key), }; }), - "m.device_id": deviceId, - "m.call_id": "", + device_id: deviceId, + call_id: "", } as EncryptionKeysEventContent); logger.debug( @@ -435,8 +435,8 @@ export class MatrixRTCSession extends TypedEventEmitter(); - const deviceId = content["m.device_id"]; - const callId = content["m.call_id"]; + const deviceId = content["device_id"]; + const callId = content["call_id"]; if (!userId) { logger.warn(`Received m.call.encryption_keys with no userId: callId=${callId}`); diff --git a/src/matrixrtc/types.ts b/src/matrixrtc/types.ts index 0cdbf70e513..21a55f46052 100644 --- a/src/matrixrtc/types.ts +++ b/src/matrixrtc/types.ts @@ -20,7 +20,7 @@ export interface EncryptionKeyEntry { } export interface EncryptionKeysEventContent { - "keys": EncryptionKeyEntry[]; - "m.device_id": string; - "m.call_id": string; + keys: EncryptionKeyEntry[]; + device_id: string; + call_id: string; } From 48051383dd7a49836f9e5cd0a2191525a48c437d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Oct 2023 18:38:05 +0100 Subject: [PATCH 101/113] Test key event retries --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 33 +++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 88dc89a46f1..59c43b1317c 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventTimeline, EventType, MatrixClient, Room } from "../../../src"; +import { EventTimeline, EventType, MatrixClient, MatrixError, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { randomString } from "../../../src/randomstring"; @@ -332,6 +332,37 @@ describe("MatrixRTCSession", () => { ], }); }); + + it("retries key sends", async () => { + jest.useFakeTimers(); + let firstEventSent = false; + + try { + const eventSentPromise = new Promise((resolve) => { + sendEventMock.mockImplementation(() => { + if (!firstEventSent) { + jest.advanceTimersByTime(10000); + + firstEventSent = true; + const e = new Error() as MatrixError; + e.data = {}; + throw e; + } else { + resolve(); + } + }); + }); + + sess!.joinRoomSession([mockFocus], true); + jest.advanceTimersByTime(10000); + + await eventSentPromise; + + expect(sendEventMock).toHaveBeenCalledTimes(2); + } finally { + jest.useRealTimers(); + } + }); }); it("emits an event at the time a membership event expires", () => { From be3539640ce3311f84f5464462aa4270b0fbde4c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Oct 2023 19:05:43 +0100 Subject: [PATCH 102/113] Test onCallEncryption --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 52 +++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index 59c43b1317c..b6d150e9720 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventTimeline, EventType, MatrixClient, MatrixError, Room } from "../../../src"; +import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { randomString } from "../../../src/randomstring"; @@ -473,4 +473,54 @@ describe("MatrixRTCSession", () => { "@alice:example.org", ); }); + + it("collects keys from encryption events", () => { + const mockRoom = makeMockRoom([membershipTemplate]); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess.onCallEncryption({ + getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"), + getContent: jest.fn().mockReturnValue({ + device_id: "bobsphone", + call_id: "", + keys: [ + { + index: 0, + key: "dGhpcyBpcyB0aGUga2V5", + }, + ], + }), + getSender: jest.fn().mockReturnValue("@bob:example.org"), + } as unknown as MatrixEvent); + + const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!; + expect(bobKeys).toHaveLength(1); + expect(bobKeys[0]).toEqual(Buffer.from("this is the key", "utf-8")); + }); + + it("collects keys at non-zero indices", () => { + const mockRoom = makeMockRoom([membershipTemplate]); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + sess.onCallEncryption({ + getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"), + getContent: jest.fn().mockReturnValue({ + device_id: "bobsphone", + call_id: "", + keys: [ + { + index: 4, + key: "dGhpcyBpcyB0aGUga2V5", + }, + ], + }), + getSender: jest.fn().mockReturnValue("@bob:example.org"), + } as unknown as MatrixEvent); + + const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!; + expect(bobKeys).toHaveLength(5); + expect(bobKeys[0]).toBeFalsy(); + expect(bobKeys[1]).toBeFalsy(); + expect(bobKeys[2]).toBeFalsy(); + expect(bobKeys[3]).toBeFalsy(); + expect(bobKeys[4]).toEqual(Buffer.from("this is the key", "utf-8")); + }); }); From a36124360cb44b80f7d0f240f900083bc782033d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Oct 2023 12:10:15 +0100 Subject: [PATCH 103/113] Test event sending & spam prevention --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 106 ++++++++++++++++++- spec/unit/matrixrtc/mocks.ts | 6 +- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index b6d150e9720..c38a153b8da 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -18,7 +18,7 @@ import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { randomString } from "../../../src/randomstring"; -import { makeMockRoom, mockRTCEvent } from "./mocks"; +import { makeMockRoom, makeMockRoomState, mockRTCEvent } from "./mocks"; const membershipTemplate: CallMembershipData = { call_id: "", @@ -363,6 +363,110 @@ describe("MatrixRTCSession", () => { jest.useRealTimers(); } }); + + it("Re-sends key if a new member joins", async () => { + jest.useFakeTimers(); + try { + const mockRoom = makeMockRoom([membershipTemplate]); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + + const keysSentPromise1 = new Promise((resolve) => { + sendEventMock.mockImplementation(resolve); + }); + + sess.joinRoomSession([mockFocus], true); + await keysSentPromise1; + + sendEventMock.mockClear(); + jest.advanceTimersByTime(10000); + + const keysSentPromise2 = new Promise((resolve) => { + sendEventMock.mockImplementation(resolve); + }); + + const onMembershipsChanged = jest.fn(); + sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged); + + const member2 = Object.assign({}, membershipTemplate, { + device_id: "BBBBBBB", + }); + + mockRoom.getLiveTimeline().getState = jest + .fn() + .mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId, undefined)); + sess.onMembershipUpdate(); + + await keysSentPromise2; + + expect(sendEventMock).toHaveBeenCalled(); + } finally { + jest.useRealTimers(); + } + }); + + it("Doesn't re-send key immediately", async () => { + const realSetImmediate = setImmediate; + jest.useFakeTimers(); + try { + const mockRoom = makeMockRoom([membershipTemplate]); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + + const keysSentPromise1 = new Promise((resolve) => { + sendEventMock.mockImplementation(resolve); + }); + + sess.joinRoomSession([mockFocus], true); + await keysSentPromise1; + + sendEventMock.mockClear(); + + const onMembershipsChanged = jest.fn(); + sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged); + + const member2 = Object.assign({}, membershipTemplate, { + device_id: "BBBBBBB", + }); + + mockRoom.getLiveTimeline().getState = jest + .fn() + .mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId, undefined)); + sess.onMembershipUpdate(); + + await new Promise((resolve) => { + realSetImmediate(resolve); + }); + + expect(sendEventMock).not.toHaveBeenCalled(); + } finally { + jest.useRealTimers(); + } + }); + }); + + it("Does not emits if no membership changes", () => { + const mockRoom = makeMockRoom([membershipTemplate]); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + + const onMembershipsChanged = jest.fn(); + sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged); + sess.onMembershipUpdate(); + + expect(onMembershipsChanged).not.toHaveBeenCalled(); + }); + + it("Emits on membership changes", () => { + const mockRoom = makeMockRoom([membershipTemplate]); + sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); + + const onMembershipsChanged = jest.fn(); + sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged); + + mockRoom.getLiveTimeline().getState = jest + .fn() + .mockReturnValue(makeMockRoomState([], mockRoom.roomId, undefined)); + sess.onMembershipUpdate(); + + expect(onMembershipsChanged).toHaveBeenCalled(); }); it("emits an event at the time a membership event expires", () => { diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index fa7d948e620..f710c49ab7f 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -31,7 +31,11 @@ export function makeMockRoom( } as unknown as Room; } -function makeMockRoomState(memberships: CallMembershipData[], roomId: string, getLocalAge: (() => number) | undefined) { +export function makeMockRoomState( + memberships: CallMembershipData[], + roomId: string, + getLocalAge: (() => number) | undefined, +) { return { getStateEvents: (_: string, stateKey: string) => { const event = mockRTCEvent(memberships, roomId, getLocalAge); From bd279ee576f1e8524c583e95116caabe3d1f1b47 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Oct 2023 12:24:04 +0100 Subject: [PATCH 104/113] Test event cancelation --- spec/unit/matrixrtc/MatrixRTCSession.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index c38a153b8da..046dea947a1 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -364,6 +364,22 @@ describe("MatrixRTCSession", () => { } }); + it("cancels key send event that fail", async () => { + const eventSentinel = {} as unknown as MatrixEvent; + + client.cancelPendingEvent = jest.fn(); + sendEventMock.mockImplementation(() => { + const e = new Error() as MatrixError; + e.data = {}; + e.event = eventSentinel; + throw e; + }); + + sess!.joinRoomSession([mockFocus], true); + + expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel); + }); + it("Re-sends key if a new member joins", async () => { jest.useFakeTimers(); try { From 3dd4ffb6e2cf1efa5ae37b62ba55b1ebb37739c0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Oct 2023 12:42:06 +0100 Subject: [PATCH 105/113] Test onCallEncryption called --- .../matrixrtc/MatrixRTCSessionManager.spec.ts | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index 6a240831e94..8784ab48b48 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -14,7 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ClientEvent, EventTimeline, MatrixClient } from "../../../src"; +import { + ClientEvent, + EventTimeline, + EventType, + IRoomTimelineData, + MatrixClient, + MatrixEvent, + RoomEvent, +} from "../../../src"; import { RoomStateEvent } from "../../../src/models/room-state"; import { CallMembershipData } from "../../../src/matrixrtc/CallMembership"; import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager"; @@ -78,4 +86,26 @@ describe("MatrixRTCSessionManager", () => { expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1)); }); + + it("Calls onCallEncryption on encryption keys event", () => { + const room1 = makeMockRoom([membershipTemplate]); + jest.spyOn(client, "getRooms").mockReturnValue([room1]); + jest.spyOn(client, "getRoom").mockReturnValue(room1); + + client.emit(ClientEvent.Room, room1); + const onCallEncryptionMock = jest.fn(); + client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock; + + const timelineEvent = { + getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix), + getContent: jest.fn().mockReturnValue({}), + getSender: jest.fn().mockReturnValue("@mock:user.example"), + getRoomId: jest.fn().mockReturnValue("!room:id"), + sender: { + userId: "@mock:user.example", + }, + } as unknown as MatrixEvent; + client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData); + expect(onCallEncryptionMock).toHaveBeenCalled(); + }); }); From 8b544b6ba90f8027027bd7e4d0ffc45c9c72e5f3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 25 Oct 2023 14:41:25 +0100 Subject: [PATCH 106/113] Some errors didn't have data --- src/matrixrtc/MatrixRTCSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 607c68d9623..b2daf15dac3 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -396,7 +396,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Wed, 25 Oct 2023 15:29:02 +0100 Subject: [PATCH 107/113] Fix binary key comparison & add log line --- src/matrixrtc/MatrixRTCSession.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index b2daf15dac3..1a87dbf7c9f 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -35,6 +35,10 @@ const UPDATE_ENCRYPTION_KEY_THROTTLE = 3000; const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`; const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId); +function keysEqual(a: Uint8Array, b: Uint8Array): boolean { + return a.length === b.length && a.every((x, i) => x === b[i]); +} + export enum MatrixRTCSessionEvent { // A member joined, left, or updated a property of their membership. MembershipsChanged = "memberships_changed", @@ -302,7 +306,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Wed, 25 Oct 2023 15:41:43 +0100 Subject: [PATCH 108/113] Fix compare function with undefined values --- src/matrixrtc/MatrixRTCSession.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 1a87dbf7c9f..ee5584cae0b 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -36,7 +36,8 @@ const getParticipantId = (userId: string, deviceId: string): string => `${userId const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId); function keysEqual(a: Uint8Array, b: Uint8Array): boolean { - return a.length === b.length && a.every((x, i) => x === b[i]); + if (a === b) return true; + return a && b && a.length === b.length && a.every((x, i) => x === b[i]); } export enum MatrixRTCSessionEvent { From 858f67f73bc1c0a24be1db9e9df887c31c876c35 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 26 Oct 2023 17:00:26 +0100 Subject: [PATCH 109/113] Remove more key logging --- src/matrixrtc/MatrixRTCSession.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index ee5584cae0b..e32d0bcf1d3 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -392,7 +392,6 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 30 Oct 2023 16:36:12 +0000 Subject: [PATCH 110/113] Check content.keys is an array --- src/matrixrtc/MatrixRTCSession.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index e32d0bcf1d3..8db0199793e 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -456,6 +456,11 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 30 Oct 2023 16:39:23 +0000 Subject: [PATCH 111/113] Check key index & key --- src/matrixrtc/MatrixRTCSession.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 8db0199793e..e438692835a 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -26,6 +26,7 @@ import { MatrixError, MatrixEvent } from "../matrix"; import { randomString, secureRandomBase64 } from "../randomstring"; import { EncryptionKeysEventContent } from "./types"; import { decodeBase64, encodeUnpaddedBase64 } from "../base64"; +import { isNumber } from "../utils"; const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000; const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event @@ -462,6 +463,11 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 30 Oct 2023 16:54:36 +0000 Subject: [PATCH 112/113] Better function name --- src/matrixrtc/MatrixRTCSession.ts | 4 ++-- src/randomstring.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index e438692835a..43348ff3ec2 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -23,7 +23,7 @@ import { EventType } from "../@types/event"; import { CallMembership, CallMembershipData } from "./CallMembership"; import { Focus } from "./focus"; import { MatrixError, MatrixEvent } from "../matrix"; -import { randomString, secureRandomBase64 } from "../randomstring"; +import { randomString, secureRandomBase64Url } from "../randomstring"; import { EncryptionKeysEventContent } from "./types"; import { decodeBase64, encodeUnpaddedBase64 } from "../base64"; import { isNumber } from "../utils"; @@ -325,7 +325,7 @@ export class MatrixRTCSession extends TypedEventEmitter Date: Mon, 30 Oct 2023 16:56:31 +0000 Subject: [PATCH 113/113] Tests too --- spec/unit/randomstring.spec.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/spec/unit/randomstring.spec.ts b/spec/unit/randomstring.spec.ts index 8194c9186a6..526edfacfcd 100644 --- a/spec/unit/randomstring.spec.ts +++ b/spec/unit/randomstring.spec.ts @@ -15,12 +15,17 @@ limitations under the License. */ import { decodeBase64 } from "../../src/base64"; -import { randomLowercaseString, randomString, randomUppercaseString, secureRandomBase64 } from "../../src/randomstring"; +import { + randomLowercaseString, + randomString, + randomUppercaseString, + secureRandomBase64Url, +} from "../../src/randomstring"; describe("Random strings", () => { it.each([8, 16, 32])("secureRandomBase64 generates %i valid base64 bytes", (n: number) => { - const randb641 = secureRandomBase64(n); - const randb642 = secureRandomBase64(n); + const randb641 = secureRandomBase64Url(n); + const randb642 = secureRandomBase64Url(n); expect(randb641).not.toEqual(randb642);