Merge pull request #1704 from famedly/td/forgiveNotKill
fix: ignore expired calls rather than killing them
This commit is contained in:
commit
38b9e773f5
|
|
@ -1794,8 +1794,6 @@ class Client extends MatrixApi {
|
||||||
await processToDeviceQueue();
|
await processToDeviceQueue();
|
||||||
} catch (_) {} // we want to dispose any errors this throws
|
} catch (_) {} // we want to dispose any errors this throws
|
||||||
|
|
||||||
await singleShotStaleCallChecker();
|
|
||||||
|
|
||||||
_retryDelay = Future.value();
|
_retryDelay = Future.value();
|
||||||
onSyncStatus.add(SyncStatusUpdate(SyncStatus.finished));
|
onSyncStatus.add(SyncStatusUpdate(SyncStatus.finished));
|
||||||
} on MatrixException catch (e, s) {
|
} on MatrixException catch (e, s) {
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,9 @@ class GroupCall {
|
||||||
Event? getMemberStateEvent(String userId) {
|
Event? getMemberStateEvent(String userId) {
|
||||||
final event = room.getState(EventTypes.GroupCallMemberPrefix, userId);
|
final event = room.getState(EventTypes.GroupCallMemberPrefix, userId);
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
return room.callMemberStateIsExpired(event, groupCallId) ? null : event;
|
return room.callMemberStateForIdIsExpired(event, groupCallId)
|
||||||
|
? null
|
||||||
|
: event;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -279,7 +281,7 @@ class GroupCall {
|
||||||
roomStates.sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
|
roomStates.sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
|
||||||
for (final value in roomStates) {
|
for (final value in roomStates) {
|
||||||
if (value.type == EventTypes.GroupCallMemberPrefix &&
|
if (value.type == EventTypes.GroupCallMemberPrefix &&
|
||||||
!room.callMemberStateIsExpired(value, groupCallId)) {
|
!room.callMemberStateForIdIsExpired(value, groupCallId)) {
|
||||||
events.add(value);
|
events.add(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
@ -13,7 +11,7 @@ extension GroupCallUtils on Room {
|
||||||
states.tryGetMap<String, Event>(EventTypes.GroupCallMemberPrefix);
|
states.tryGetMap<String, Event>(EventTypes.GroupCallMemberPrefix);
|
||||||
if (groupCallMemberStates != null) {
|
if (groupCallMemberStates != null) {
|
||||||
groupCallMemberStates.forEach((userId, memberStateEvent) {
|
groupCallMemberStates.forEach((userId, memberStateEvent) {
|
||||||
if (!callMemberStateIsExpired(memberStateEvent, groupCallId)) {
|
if (!callMemberStateForIdIsExpired(memberStateEvent, groupCallId)) {
|
||||||
participantCount++;
|
participantCount++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -37,7 +35,9 @@ extension GroupCallUtils on Room {
|
||||||
.toList()
|
.toList()
|
||||||
.sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
|
.sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
|
||||||
return groupCallStates.values
|
return groupCallStates.values
|
||||||
.where((element) => !element.content.containsKey('m.terminated'))
|
.where((element) =>
|
||||||
|
!element.content.containsKey('m.terminated') &&
|
||||||
|
callMemberStateIsExpired(element))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -45,7 +45,22 @@ extension GroupCallUtils on Room {
|
||||||
|
|
||||||
static const staleCallCheckerDuration = Duration(seconds: 30);
|
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) {
|
MatrixEvent groupCallMemberStateEvent, String groupCallId) {
|
||||||
final callMemberState =
|
final callMemberState =
|
||||||
IGroupCallRoomMemberState.fromJson(groupCallMemberStateEvent);
|
IGroupCallRoomMemberState.fromJson(groupCallMemberStateEvent);
|
||||||
|
|
@ -71,7 +86,6 @@ extension GroupCallUtils on Room {
|
||||||
// whose state event we haven't recieved yet in sync.
|
// whose state event we haven't recieved yet in sync.
|
||||||
// (option 2 was local echo member state events, but reverting them if anything
|
// (option 2 was local echo member state events, but reverting them if anything
|
||||||
// fails sounds pain)
|
// fails sounds pain)
|
||||||
|
|
||||||
final expiredfr = groupCallMemberStateEvent.originServerTs
|
final expiredfr = groupCallMemberStateEvent.originServerTs
|
||||||
.add(staleCallCheckerDuration)
|
.add(staleCallCheckerDuration)
|
||||||
.millisecondsSinceEpoch <
|
.millisecondsSinceEpoch <
|
||||||
|
|
@ -84,88 +98,4 @@ extension GroupCallUtils on Room {
|
||||||
return expiredfr;
|
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<void> singleShotStaleCallCheckerOnRoom() async {
|
|
||||||
if (partial) return;
|
|
||||||
await client.oneShotSync();
|
|
||||||
|
|
||||||
final copyGroupCallIds =
|
|
||||||
states.tryGetMap<String, Event>(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<String, Event>(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<String?> 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<void> singleShotStaleCallChecker() async {
|
|
||||||
if (lastStaleCallRun
|
|
||||||
.add(GroupCallUtils.staleCallCheckerDuration)
|
|
||||||
.isBefore(DateTime.now())) {
|
|
||||||
await Future.wait(rooms
|
|
||||||
.where((r) => r.membership == Membership.join)
|
|
||||||
.map((r) => r.singleShotStaleCallCheckerOnRoom()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1429,7 +1429,7 @@ void main() {
|
||||||
|
|
||||||
test('callMemberStateIsExpired', () {
|
test('callMemberStateIsExpired', () {
|
||||||
expect(
|
expect(
|
||||||
room.callMemberStateIsExpired(
|
room.callMemberStateForIdIsExpired(
|
||||||
Event(
|
Event(
|
||||||
senderId: '@test:example.com',
|
senderId: '@test:example.com',
|
||||||
type: EventTypes.GroupCallMemberPrefix,
|
type: EventTypes.GroupCallMemberPrefix,
|
||||||
|
|
@ -1457,7 +1457,7 @@ void main() {
|
||||||
'1674811248673789288k7d60n5976'),
|
'1674811248673789288k7d60n5976'),
|
||||||
true);
|
true);
|
||||||
expect(
|
expect(
|
||||||
room.callMemberStateIsExpired(
|
room.callMemberStateForIdIsExpired(
|
||||||
Event(
|
Event(
|
||||||
senderId: '@test:example.com',
|
senderId: '@test:example.com',
|
||||||
type: EventTypes.GroupCallMemberPrefix,
|
type: EventTypes.GroupCallMemberPrefix,
|
||||||
|
|
@ -1488,37 +1488,6 @@ void main() {
|
||||||
false);
|
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', () {
|
test('group call participants count', () {
|
||||||
room.setState(
|
room.setState(
|
||||||
Event(
|
Event(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue