From d7d4c61a9d6351251c3ae1b95d3b9ed10ce97003 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 8 Jun 2022 17:08:34 -0400 Subject: [PATCH 1/3] Expire call member state events after 1 hour --- src/webrtc/groupCall.ts | 117 ++++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 45 deletions(-) diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index 0b54e3a9809..da66d0abd85 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -109,6 +109,7 @@ export interface IGroupCallRoomMemberCallState { export interface IGroupCallRoomMemberState { "m.calls": IGroupCallRoomMemberCallState[]; + "m.expires_ts": number; } export enum GroupCallState { @@ -127,6 +128,16 @@ interface ICallHandlers { onCallReplaced: (newCall: MatrixCall) => void; } +const CALL_MEMBER_STATE_TIMEOUT = 1000 * 60 * 60; // 1 hour + +const callMemberStateIsExpired = (event: MatrixEvent): boolean => { + const now = Date.now(); + const content = event?.getContent() ?? {}; + const expiresAt = typeof content["m.expires_ts"] === "number" ? content["m.expires_ts"] : -Infinity; + // The event is expired if the expiration date has passed, or if it's unreasonably far in the future + return expiresAt <= now || expiresAt > now + CALL_MEMBER_STATE_TIMEOUT * 5 / 4; +} + function getCallUserId(call: MatrixCall): string | null { return call.getOpponentMember()?.userId || call.invitee || null; } @@ -155,6 +166,8 @@ export class GroupCall extends TypedEventEmitter = new Map(); private reEmitter: ReEmitter; private transmitTimer: ReturnType | null = null; + private memberStateExpirationTimers: Map> = new Map(); + private resendMemberStateTimer: ReturnType | null = null; constructor( private client: MatrixClient, @@ -170,10 +183,7 @@ export class GroupCall extends TypedEventEmitter { - const deviceId = this.client.getDeviceId(); + private getMemberStateEvents: () => MatrixEvent[]; + private getMemberStateEvents: (userId: string) => MatrixEvent | null; + private getMemberStateEvents = (userId?: string) => { + if (userId) { + const event = this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, userId); + return callMemberStateIsExpired(event) ? null : event; + } else { + return this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix) + .filter(event => !callMemberStateIsExpired(event)); + } + } - return this.updateMemberCallState({ + private async sendMemberStateEvent(): Promise { + const send = () => this.updateMemberCallState({ "m.call_id": this.groupCallId, "m.devices": [ { - "device_id": deviceId, + "device_id": this.client.getDeviceId(), "session_id": this.client.getSessionId(), "feeds": this.getLocalFeeds().map((feed) => ({ purpose: feed.purpose, @@ -645,23 +657,35 @@ export class GroupCall extends TypedEventEmitter { + logger.log("Resending call member state"); + await send(); + }, CALL_MEMBER_STATE_TIMEOUT * 3 / 4); + + return res; } - private removeMemberStateEvent(): Promise { - return this.updateMemberCallState(undefined); + private async removeMemberStateEvent(): Promise { + const res = await this.updateMemberCallState(undefined); + clearInterval(this.resendMemberStateTimer); + this.resendMemberStateTimer = null; + return res; } private async updateMemberCallState(memberCallState?: IGroupCallRoomMemberCallState): Promise { const localUserId = this.client.getUserId(); - const currentStateEvent = this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, localUserId); - const memberStateEvent = currentStateEvent?.getContent(); + const memberState = this.getMemberStateEvents(localUserId)?.getContent(); let calls: IGroupCallRoomMemberCallState[] = []; // Sanitize existing member state event - if (memberStateEvent && Array.isArray(memberStateEvent["m.calls"])) { - calls = memberStateEvent["m.calls"].filter((call) => !!call); + if (memberState && Array.isArray(memberState["m.calls"])) { + calls = memberState["m.calls"].filter((call) => !!call); } const existingCallIndex = calls.findIndex((call) => call && call["m.call_id"] === this.groupCallId); @@ -678,6 +702,7 @@ export class GroupCall extends TypedEventEmitter { // The member events may be received for another room, which we will ignore. - if (event.getRoomId() !== this.room.roomId) { - return; - } + if (event.getRoomId() !== this.room.roomId) return; const member = this.room.getMember(event.getStateKey()); + if (!member) return; - if (!member) { - return; - } - - let callsState = event.getContent()["m.calls"]; + const ignore = () => { + this.removeParticipant(member); + clearTimeout(this.memberStateExpirationTimers.get(member.userId)); + this.memberStateExpirationTimers.delete(member.userId); + }; - if (Array.isArray(callsState)) { - callsState = callsState.filter((call) => !!call); - } + const content = event.getContent(); + let callsState = !callMemberStateIsExpired(event) && Array.isArray(content["m.calls"]) + ? content["m.calls"].filter((call) => call) + : []; // Ignore expired device data - if (!Array.isArray(callsState) || callsState.length === 0) { - logger.warn(`Ignoring member state from ${member.userId} member not in any calls.`); - this.removeParticipant(member); + if (callsState.length === 0) { + logger.log(`Ignoring member state from ${member.userId} member not in any calls.`); + ignore(); return; } // Currently we only support a single call per room. So grab the first call. const callState = callsState[0]; - const callId = callState["m.call_id"]; if (!callId) { logger.warn(`Room member ${member.userId} does not have a valid m.call_id set. Ignoring.`); - this.removeParticipant(member); + ignore(); return; } if (callId !== this.groupCallId) { logger.warn(`Call id ${callId} does not match group call id ${this.groupCallId}, ignoring.`); - this.removeParticipant(member); + ignore(); return; } this.addParticipant(member); + clearTimeout(this.memberStateExpirationTimers.get(member.userId)); + this.memberStateExpirationTimers.set(member.userId, setTimeout(() => { + logger.warn(`Call member state for ${member.userId} has expired`); + this.removeParticipant(member); + }, content["m.expires_ts"] - Date.now())); + // Don't process your own member. const localUserId = this.client.getUserId(); @@ -813,7 +843,7 @@ export class GroupCall extends TypedEventEmitter { - const roomState = this.room.currentState; - const memberStateEvents = roomState.getStateEvents(EventType.GroupCallMemberPrefix); - - for (const event of memberStateEvents) { + for (const event of this.getMemberStateEvents()) { const memberId = event.getStateKey(); const existingCall = this.calls.find((call) => getCallUserId(call) === memberId); const retryCallCount = this.retryCallCounts.get(memberId) || 0; From 792e712b0c22e430742f4e42cf8e1c627de56124 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 8 Jun 2022 17:31:11 -0400 Subject: [PATCH 2/3] Fix lints --- src/webrtc/groupCall.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index da66d0abd85..c98816cb19f 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -12,7 +12,6 @@ import { ISendEventResponse } from "../@types/requests"; import { MatrixEvent } from "../models/event"; import { EventType } from "../@types/event"; import { CallEventHandlerEvent } from "./callEventHandler"; -import { RoomStateEvent } from "../matrix"; import { GroupCallEventHandlerEvent } from "./groupCallEventHandler"; export enum GroupCallIntent { @@ -136,7 +135,7 @@ const callMemberStateIsExpired = (event: MatrixEvent): boolean => { const expiresAt = typeof content["m.expires_ts"] === "number" ? content["m.expires_ts"] : -Infinity; // The event is expired if the expiration date has passed, or if it's unreasonably far in the future return expiresAt <= now || expiresAt > now + CALL_MEMBER_STATE_TIMEOUT * 5 / 4; -} +}; function getCallUserId(call: MatrixCall): string | null { return call.getOpponentMember()?.userId || call.invitee || null; @@ -630,10 +629,10 @@ export class GroupCall extends TypedEventEmitter MatrixEvent[]; - private getMemberStateEvents: (userId: string) => MatrixEvent | null; - private getMemberStateEvents = (userId?: string) => { - if (userId) { + private getMemberStateEvents(): MatrixEvent[]; + private getMemberStateEvents(userId: string): MatrixEvent | null; + private getMemberStateEvents(userId?: string): MatrixEvent[] | MatrixEvent | null { + if (userId != null) { const event = this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, userId); return callMemberStateIsExpired(event) ? null : event; } else { @@ -722,7 +721,7 @@ export class GroupCall extends TypedEventEmitter(); - let callsState = !callMemberStateIsExpired(event) && Array.isArray(content["m.calls"]) + const callsState = !callMemberStateIsExpired(event) && Array.isArray(content["m.calls"]) ? content["m.calls"].filter((call) => call) : []; // Ignore expired device data From 1dd2a939917bd9d0e7fa1b53c4c9922577715d46 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 16 Jun 2022 11:20:43 -0400 Subject: [PATCH 3/3] Avoid a possible race --- src/webrtc/groupCall.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index 77bbc3c6b7c..0b12f97f9e5 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -703,10 +703,9 @@ export class GroupCall extends TypedEventEmitter { - const res = await this.updateMemberCallState(undefined); clearInterval(this.resendMemberStateTimer); this.resendMemberStateTimer = null; - return res; + return await this.updateMemberCallState(undefined); } private async updateMemberCallState(memberCallState?: IGroupCallRoomMemberCallState): Promise {