diff --git a/spec/unit/matrixrtc/CallMembership.spec.ts b/spec/unit/matrixrtc/CallMembership.spec.ts index b72d1c79877..3d259103cc5 100644 --- a/spec/unit/matrixrtc/CallMembership.spec.ts +++ b/spec/unit/matrixrtc/CallMembership.spec.ts @@ -34,9 +34,12 @@ function makeMockEvent(originTs = 0): MatrixEvent { } describe("CallMembership", () => { - it("rejects membership with no expiry", () => { + it("rejects membership with no expiry and no expires_ts", () => { expect(() => { - new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: undefined })); + new CallMembership( + makeMockEvent(), + Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }), + ); }).toThrow(); }); @@ -57,6 +60,16 @@ describe("CallMembership", () => { new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined })); }).toThrow(); }); + it("rejects with malformatted expires_ts", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires_ts: "string" })); + }).toThrow(); + }); + it("rejects with malformatted expires", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: "string" })); + }).toThrow(); + }); it("uses event timestamp if no created_ts", () => { const membership = new CallMembership(makeMockEvent(12345), membershipTemplate); @@ -71,11 +84,19 @@ describe("CallMembership", () => { expect(membership.createdTs()).toEqual(67890); }); - it("computes absolute expiry time", () => { + it("computes absolute expiry time based on expires", () => { const membership = new CallMembership(makeMockEvent(1000), membershipTemplate); expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000); }); + it("computes absolute expiry time based on expires_ts", () => { + const membership = new CallMembership( + makeMockEvent(1000), + Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: 6000 }), + ); + 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); diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index af71a90b06f..c1faa5ac0c3 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -214,8 +214,8 @@ describe("MatrixRTCSession", () => { }); it("sends a membership event when joining a call", () => { + jest.useFakeTimers(); sess!.joinRoomSession([mockFocus]); - expect(client.sendStateEvent).toHaveBeenCalledWith( mockRoom!.roomId, EventType.GroupCallMemberPrefix, @@ -227,6 +227,7 @@ describe("MatrixRTCSession", () => { call_id: "", device_id: "AAAAAAA", expires: 3600000, + expires_ts: Date.now() + 3600000, foci_active: [{ type: "mock" }], membershipID: expect.stringMatching(".*"), }, @@ -234,6 +235,7 @@ describe("MatrixRTCSession", () => { }, "@alice:example.org", ); + jest.useRealTimers(); }); it("does nothing if join called when already joined", () => { @@ -291,6 +293,7 @@ describe("MatrixRTCSession", () => { call_id: "", device_id: "AAAAAAA", expires: 3600000 * 2, + expires_ts: 1000 + 3600000 * 2, foci_active: [{ type: "mock" }], created_ts: 1000, membershipID: expect.stringMatching(".*"), @@ -510,7 +513,7 @@ describe("MatrixRTCSession", () => { }); }); - it("Does not emits if no membership changes", () => { + it("Does not emit if no membership changes", () => { const mockRoom = makeMockRoom([membershipTemplate]); sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom); @@ -591,6 +594,7 @@ describe("MatrixRTCSession", () => { call_id: "", device_id: "AAAAAAA", expires: 3600000, + expires_ts: Date.now() + 3600000, foci_active: [mockFocus], membershipID: expect.stringMatching(".*"), }, @@ -605,7 +609,7 @@ describe("MatrixRTCSession", () => { it("fills in created_ts for other memberships on update", () => { client.sendStateEvent = jest.fn(); - + jest.useFakeTimers(); const mockRoom = makeMockRoom([ Object.assign({}, membershipTemplate, { device_id: "OTHERDEVICE", @@ -635,6 +639,7 @@ describe("MatrixRTCSession", () => { call_id: "", device_id: "AAAAAAA", expires: 3600000, + expires_ts: Date.now() + 3600000, foci_active: [mockFocus], membershipID: expect.stringMatching(".*"), }, @@ -642,6 +647,7 @@ describe("MatrixRTCSession", () => { }, "@alice:example.org", ); + jest.useRealTimers(); }); it("collects keys from encryption events", () => { diff --git a/src/matrixrtc/CallMembership.ts b/src/matrixrtc/CallMembership.ts index 917e36f0da9..171751f1d1f 100644 --- a/src/matrixrtc/CallMembership.ts +++ b/src/matrixrtc/CallMembership.ts @@ -27,7 +27,8 @@ export interface CallMembershipData { scope: CallScope; device_id: string; created_ts?: number; - expires: number; + expires?: number; + expires_ts?: number; foci_active?: Focus[]; membershipID: string; } @@ -41,7 +42,20 @@ export class CallMembership { private parentEvent: MatrixEvent, private data: CallMembershipData, ) { - if (typeof data.expires !== "number") throw new Error("Malformed membership: expires must be numeric"); + if (!(data.expires || data.expires_ts)) { + throw new Error("Malformed membership: expires_ts or expires must be present"); + } + if (data.expires) { + if (typeof data.expires !== "number") { + throw new Error("Malformed membership: expires must be numeric"); + } + } + if (data.expires_ts) { + if (typeof data.expires_ts !== "number") { + throw new Error("Malformed membership: expires_ts 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"); @@ -77,16 +91,27 @@ export class CallMembership { } public getAbsoluteExpiry(): number { - return this.createdTs() + this.data.expires; + if (this.data.expires) { + return this.createdTs() + this.data.expires; + } else { + // We know it exists because we checked for this in the constructor. + return this.data.expires_ts!; + } } // gets the expiry time of the event, converted into the device's local time public getLocalExpiry(): number { - const relativeCreationTime = this.parentEvent.getTs() - this.createdTs(); + if (this.data.expires) { + const relativeCreationTime = this.parentEvent.getTs() - this.createdTs(); - const localCreationTs = this.parentEvent.localTimestamp - relativeCreationTime; + const localCreationTs = this.parentEvent.localTimestamp - relativeCreationTime; - return localCreationTs + this.data.expires; + return localCreationTs + this.data.expires; + } else { + // With expires_ts we cannot convert to local time. + // TODO: Check the server timestamp and compute a diff to local time. + return this.data.expires_ts!; + } } public getMsUntilExpiry(): number { diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index ec06e58bea3..b8ee6626fd0 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -624,6 +624,9 @@ export class MatrixRTCSession extends TypedEventEmitter