From aa77812e8e54d3812aaf8d0d4ffa9e1ebda08916 Mon Sep 17 00:00:00 2001 From: td Date: Wed, 14 Feb 2024 03:27:09 +0530 Subject: [PATCH] fix: ignore expired calls rather than killing them --- lib/src/client.dart | 2 - lib/src/voip/group_call.dart | 6 +- lib/src/voip/voip_room_extension.dart | 110 +++++--------------------- test/room_test.dart | 35 +------- 4 files changed, 26 insertions(+), 127 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index db21b511..e2650f7e 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1794,8 +1794,6 @@ class Client extends MatrixApi { await processToDeviceQueue(); } catch (_) {} // we want to dispose any errors this throws - await singleShotStaleCallChecker(); - _retryDelay = Future.value(); onSyncStatus.add(SyncStatusUpdate(SyncStatus.finished)); } on MatrixException catch (e, s) { diff --git a/lib/src/voip/group_call.dart b/lib/src/voip/group_call.dart index 1187b86d..1c3cd7f6 100644 --- a/lib/src/voip/group_call.dart +++ b/lib/src/voip/group_call.dart @@ -268,7 +268,9 @@ class GroupCall { Event? getMemberStateEvent(String userId) { final event = room.getState(EventTypes.GroupCallMemberPrefix, userId); if (event != null) { - return room.callMemberStateIsExpired(event, groupCallId) ? null : event; + return room.callMemberStateForIdIsExpired(event, groupCallId) + ? null + : event; } return null; } @@ -279,7 +281,7 @@ class GroupCall { roomStates.sort((a, b) => a.originServerTs.compareTo(b.originServerTs)); for (final value in roomStates) { if (value.type == EventTypes.GroupCallMemberPrefix && - !room.callMemberStateIsExpired(value, groupCallId)) { + !room.callMemberStateForIdIsExpired(value, groupCallId)) { events.add(value); } } diff --git a/lib/src/voip/voip_room_extension.dart b/lib/src/voip/voip_room_extension.dart index 54e518e4..eba4bdbb 100644 --- a/lib/src/voip/voip_room_extension.dart +++ b/lib/src/voip/voip_room_extension.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:collection/collection.dart'; import 'package:matrix/matrix.dart'; @@ -13,7 +11,7 @@ extension GroupCallUtils on Room { states.tryGetMap(EventTypes.GroupCallMemberPrefix); if (groupCallMemberStates != null) { groupCallMemberStates.forEach((userId, memberStateEvent) { - if (!callMemberStateIsExpired(memberStateEvent, groupCallId)) { + if (!callMemberStateForIdIsExpired(memberStateEvent, groupCallId)) { participantCount++; } }); @@ -37,7 +35,9 @@ extension GroupCallUtils on Room { .toList() .sort((a, b) => a.originServerTs.compareTo(b.originServerTs)); return groupCallStates.values - .where((element) => !element.content.containsKey('m.terminated')) + .where((element) => + !element.content.containsKey('m.terminated') && + callMemberStateIsExpired(element)) .toList(); } return []; @@ -45,7 +45,22 @@ extension GroupCallUtils on Room { static const staleCallCheckerDuration = Duration(seconds: 30); - bool callMemberStateIsExpired( + /// checks if a member event has any existing non-expired callId + bool callMemberStateIsExpired(MatrixEvent event) { + final callMemberState = IGroupCallRoomMemberState.fromJson(event); + final calls = callMemberState.calls; + return calls + .where((call) => call.devices.any((d) => + (d.expires_ts ?? 0) + + staleCallCheckerDuration + .inMilliseconds > // buffer for sync glare + DateTime.now().millisecondsSinceEpoch)) + .isEmpty; + } + + /// checks if the member event has `groupCallId` unexpired, if not it checks if + /// the whole event is expired or not + bool callMemberStateForIdIsExpired( MatrixEvent groupCallMemberStateEvent, String groupCallId) { final callMemberState = IGroupCallRoomMemberState.fromJson(groupCallMemberStateEvent); @@ -71,7 +86,6 @@ extension GroupCallUtils on Room { // whose state event we haven't recieved yet in sync. // (option 2 was local echo member state events, but reverting them if anything // fails sounds pain) - final expiredfr = groupCallMemberStateEvent.originServerTs .add(staleCallCheckerDuration) .millisecondsSinceEpoch < @@ -84,88 +98,4 @@ extension GroupCallUtils on Room { return expiredfr; } } - - /// checks for stale calls in a room and sends `m.terminated` if all the - /// expires_ts are expired. Called regularly on sync. - Future singleShotStaleCallCheckerOnRoom() async { - if (partial) return; - await client.oneShotSync(); - - final copyGroupCallIds = - states.tryGetMap(EventTypes.GroupCallPrefix); - if (copyGroupCallIds == null) return; - - Logs().d('[VOIP] checking for stale group calls in room $id'); - - for (final groupCall in copyGroupCallIds.entries) { - final groupCallId = groupCall.key; - final groupCallEvent = groupCall.value; - - if (groupCallEvent.content.tryGet('m.intent') == 'm.room') return; - if (!groupCallEvent.content.containsKey('m.terminated')) { - Logs().i('[VOIP] found non terminated group call with id $groupCallId'); - // call is not empty but check for stale participants (gone offline) - // with expire_ts - bool callExpired = true; // assume call is expired - final callMemberEvents = - states.tryGetMap(EventTypes.GroupCallMemberPrefix); - if (callMemberEvents != null) { - for (var i = 0; i < callMemberEvents.length; i++) { - final groupCallMemberEventMap = - callMemberEvents.entries.toList()[i]; - - final groupCallMemberEvent = groupCallMemberEventMap.value; - callExpired = - callMemberStateIsExpired(groupCallMemberEvent, groupCallId); - // no need to iterate further even if one participant says call isn't expired - if (!callExpired) break; - } - } - - if (callExpired) { - Logs().i( - '[VOIP] Group call with only expired timestamps detected, terminating'); - await sendGroupCallTerminateEvent(groupCallId); - } - } - } - } - - /// returns the event_id if successful - Future sendGroupCallTerminateEvent(String groupCallId) async { - try { - Logs().d('[VOIP] running sendterminator'); - final existingStateEvent = - getState(EventTypes.GroupCallPrefix, groupCallId); - if (existingStateEvent == null) { - Logs().e('[VOIP] could not find group call with id $groupCallId'); - return null; - } - - final req = await client - .setRoomStateWithKey(id, EventTypes.GroupCallPrefix, groupCallId, { - ...existingStateEvent.content, - 'm.terminated': GroupCallTerminationReason.CallEnded, - }); - - Logs().i('[VOIP] Group call $groupCallId was killed uwu'); - return req; - } catch (e, s) { - Logs().e('[VOIP] killing stale call $groupCallId failed', e, s); - return null; - } - } -} - -extension GroupCallClientUtils on Client { - // call after sync - Future singleShotStaleCallChecker() async { - if (lastStaleCallRun - .add(GroupCallUtils.staleCallCheckerDuration) - .isBefore(DateTime.now())) { - await Future.wait(rooms - .where((r) => r.membership == Membership.join) - .map((r) => r.singleShotStaleCallCheckerOnRoom())); - } - } } diff --git a/test/room_test.dart b/test/room_test.dart index 6b636dea..a1a2efe8 100644 --- a/test/room_test.dart +++ b/test/room_test.dart @@ -1429,7 +1429,7 @@ void main() { test('callMemberStateIsExpired', () { expect( - room.callMemberStateIsExpired( + room.callMemberStateForIdIsExpired( Event( senderId: '@test:example.com', type: EventTypes.GroupCallMemberPrefix, @@ -1457,7 +1457,7 @@ void main() { '1674811248673789288k7d60n5976'), true); expect( - room.callMemberStateIsExpired( + room.callMemberStateForIdIsExpired( Event( senderId: '@test:example.com', type: EventTypes.GroupCallMemberPrefix, @@ -1488,37 +1488,6 @@ void main() { false); }); - test('stale call checker and terminator', () async { - room.setState(Event( - content: {'m.intent': 'm.prompt', 'm.type': 'm.video'}, - type: EventTypes.GroupCallPrefix, - eventId: 'asdfasdf', - senderId: '@test:example.com', - originServerTs: DateTime.now(), - room: room, - stateKey: '1675856324414gzczMtfzTk0DKgEw')); - expect(room.hasActiveGroupCall, true); - expect(room.activeGroupCallEvents.length, 1); - expect( - await room - .sendGroupCallTerminateEvent('1675856324414gzczMtfzTk0DKgEw'), - 'groupCall'); - room.setState(Event( - content: { - 'm.intent': 'm.prompt', - 'm.type': 'm.video', - 'm.terminated': 'call_ended' - }, - type: EventTypes.GroupCallPrefix, - eventId: 'asdfasdf', - senderId: '@test:example.com', - originServerTs: DateTime.now(), - room: room, - stateKey: '1675856324414gzczMtfzTk0DKgEw')); - expect(room.hasActiveGroupCall, false); - expect(room.activeGroupCallEvents.length, 0); - }); - test('group call participants count', () { room.setState( Event(