LCOV - code coverage report
Current view: top level - lib/src/voip/utils - famedly_call_extension.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 61.7 % 141 87
Test Date: 2025-10-13 02:23:18 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:async';
       2              : 
       3              : import 'package:collection/collection.dart';
       4              : 
       5              : import 'package:matrix/matrix.dart';
       6              : 
       7              : String? _delayedLeaveEventId;
       8              : 
       9              : Timer? _restartDelayedLeaveEventTimer;
      10              : 
      11              : extension FamedlyCallMemberEventsExtension on Room {
      12              :   /// a map of every users famedly call event, holds the memberships list
      13              :   /// returns sorted according to originTs (oldest to newest)
      14            6 :   Map<String, FamedlyCallMemberEvent> getFamedlyCallEvents(VoIP voip) {
      15            6 :     final Map<String, FamedlyCallMemberEvent> mappedEvents = {};
      16              :     final famedlyCallMemberStates =
      17           12 :         states.tryGetMap<String, Event>(EventTypes.GroupCallMember);
      18              : 
      19            6 :     if (famedlyCallMemberStates == null) return {};
      20            6 :     final sortedEvents = famedlyCallMemberStates.values
      21           30 :         .sorted((a, b) => a.originServerTs.compareTo(b.originServerTs));
      22              : 
      23           12 :     for (final element in sortedEvents) {
      24            6 :       mappedEvents.addAll(
      25           18 :         {element.stateKey!: FamedlyCallMemberEvent.fromJson(element, voip)},
      26              :       );
      27              :     }
      28              :     return mappedEvents;
      29              :   }
      30              : 
      31              :   /// extracts memberships list form a famedly call event and maps it to a userid
      32              :   /// returns sorted (oldest to newest)
      33            6 :   Map<String, List<CallMembership>> getCallMembershipsFromRoom(VoIP voip) {
      34            6 :     final parsedMemberEvents = getFamedlyCallEvents(voip);
      35            6 :     final Map<String, List<CallMembership>> memberships = {};
      36           12 :     for (final element in parsedMemberEvents.entries) {
      37           30 :       memberships.addAll({element.key: element.value.memberships});
      38              :     }
      39              :     return memberships;
      40              :   }
      41              : 
      42              :   /// returns a list of memberships in the room for `user`
      43              :   /// if room version is org.matrix.msc3757.11 it also uses the deviceId
      44            4 :   List<CallMembership> getCallMembershipsForUser(
      45              :     String userId,
      46              :     String deviceId,
      47              :     VoIP voip,
      48              :   ) {
      49            4 :     final stateKey = (roomVersion?.contains('msc3757') ?? false)
      50              :         ? '${userId}_$deviceId'
      51            0 :         : userId;
      52            4 :     final parsedMemberEvents = getCallMembershipsFromRoom(voip);
      53            4 :     final mem = parsedMemberEvents.tryGet<List<CallMembership>>(stateKey);
      54            4 :     return mem ?? [];
      55              :   }
      56              : 
      57              :   /// returns the user count (not sessions, yet) for the group call with id: `groupCallId`.
      58              :   /// returns 0 if group call not found
      59            2 :   int groupCallParticipantCount(
      60              :     String groupCallId,
      61              :     VoIP voip,
      62              :   ) {
      63              :     int participantCount = 0;
      64              :     // userid:membership
      65            2 :     final memberships = getCallMembershipsFromRoom(voip);
      66              : 
      67            4 :     memberships.forEach((key, value) {
      68            4 :       for (final membership in value) {
      69            6 :         if (membership.callId == groupCallId && !membership.isExpired) {
      70            2 :           participantCount++;
      71              :         }
      72              :       }
      73              :     });
      74              : 
      75              :     return participantCount;
      76              :   }
      77              : 
      78            2 :   bool hasActiveGroupCall(VoIP voip) {
      79            4 :     if (activeGroupCallIds(voip).isNotEmpty) {
      80              :       return true;
      81              :     }
      82              :     return false;
      83              :   }
      84              : 
      85              :   /// list of active group call ids
      86            2 :   List<String> activeGroupCallIds(VoIP voip) {
      87              :     final Set<String> ids = {};
      88            2 :     final memberships = getCallMembershipsFromRoom(voip);
      89              : 
      90            4 :     memberships.forEach((key, value) {
      91            4 :       for (final mem in value) {
      92            6 :         if (!mem.isExpired) ids.add(mem.callId);
      93              :       }
      94              :     });
      95            2 :     return ids.toList();
      96              :   }
      97              : 
      98              :   /// passing no `CallMembership` removes it from the state event.
      99              :   /// Returns the event ID of the new membership state event.
     100            4 :   Future<String?> updateFamedlyCallMemberStateEvent(
     101              :     CallMembership callMembership,
     102              :   ) async {
     103            4 :     final ownMemberships = getCallMembershipsForUser(
     104            8 :       client.userID!,
     105            8 :       client.deviceID!,
     106            4 :       callMembership.voip,
     107              :     );
     108              : 
     109              :     // do not bother removing other deviceId expired events because we have no
     110              :     // ownership over them
     111              :     ownMemberships
     112           24 :         .removeWhere((element) => client.deviceID! == element.deviceId);
     113              : 
     114            4 :     ownMemberships.removeWhere((e) => e == callMembership);
     115              : 
     116            4 :     ownMemberships.add(callMembership);
     117              : 
     118            4 :     final newContent = {
     119           16 :       'memberships': List.from(ownMemberships.map((e) => e.toJson())),
     120              :     };
     121              : 
     122            4 :     return await setFamedlyCallMemberEvent(
     123              :       newContent,
     124            4 :       callMembership.voip,
     125            4 :       callMembership.callId,
     126            4 :       application: callMembership.application,
     127            4 :       scope: callMembership.scope,
     128              :     );
     129              :   }
     130              : 
     131            2 :   Future<void> removeFamedlyCallMemberEvent(
     132              :     String groupCallId,
     133              :     VoIP voip, {
     134              :     String? application = 'm.call',
     135              :     String? scope = 'm.room',
     136              :   }) async {
     137            2 :     final ownMemberships = getCallMembershipsForUser(
     138            4 :       client.userID!,
     139            4 :       client.deviceID!,
     140              :       voip,
     141              :     );
     142              : 
     143            2 :     ownMemberships.removeWhere(
     144            2 :       (mem) =>
     145            4 :           mem.callId == groupCallId &&
     146            8 :           mem.deviceId == client.deviceID! &&
     147            4 :           mem.application == application &&
     148            4 :           mem.scope == scope,
     149              :     );
     150              : 
     151            2 :     final newContent = {
     152            4 :       'memberships': List.from(ownMemberships.map((e) => e.toJson())),
     153              :     };
     154            2 :     await setFamedlyCallMemberEvent(
     155              :       newContent,
     156              :       voip,
     157              :       groupCallId,
     158              :       application: application,
     159              :       scope: scope,
     160              :     );
     161              : 
     162            0 :     _restartDelayedLeaveEventTimer?.cancel();
     163              :     if (_delayedLeaveEventId != null) {
     164            0 :       await client.manageDelayedEvent(
     165              :         _delayedLeaveEventId!,
     166              :         DelayedEventAction.cancel,
     167              :       );
     168              :       _delayedLeaveEventId = null;
     169              :     }
     170              :   }
     171              : 
     172            4 :   Future<String?> setFamedlyCallMemberEvent(
     173              :     Map<String, List> newContent,
     174              :     VoIP voip,
     175              :     String groupCallId, {
     176              :     String? application = 'm.call',
     177              :     String? scope = 'm.room',
     178              :   }) async {
     179            4 :     if (canJoinGroupCall) {
     180            4 :       final stateKey = (roomVersion?.contains('msc3757') ?? false)
     181            0 :           ? '${client.userID!}_${client.deviceID!}'
     182            8 :           : client.userID!;
     183              : 
     184            8 :       final useDelayedEvents = (await client.versionsResponse)
     185            8 :               .unstableFeatures?['org.matrix.msc4140'] ??
     186              :           false;
     187              : 
     188              :       /// can use delayed events and haven't used it yet
     189              :       if (useDelayedEvents && _delayedLeaveEventId == null) {
     190              :         // get existing ones and cancel them
     191            0 :         final List<ScheduledDelayedEvent> alreadyScheduledEvents = [];
     192              :         String? nextBatch;
     193            0 :         final sEvents = await client.getScheduledDelayedEvents();
     194            0 :         alreadyScheduledEvents.addAll(sEvents.scheduledEvents);
     195            0 :         nextBatch = sEvents.nextBatch;
     196            0 :         while (nextBatch != null || (nextBatch?.isNotEmpty ?? false)) {
     197            0 :           final res = await client.getScheduledDelayedEvents();
     198            0 :           alreadyScheduledEvents.addAll(
     199            0 :             res.scheduledEvents,
     200              :           );
     201            0 :           nextBatch = res.nextBatch;
     202              :         }
     203              : 
     204            0 :         final toCancelEvents = alreadyScheduledEvents.where(
     205            0 :           (element) => element.stateKey == stateKey,
     206              :         );
     207              : 
     208            0 :         for (final toCancelEvent in toCancelEvents) {
     209            0 :           await client.manageDelayedEvent(
     210            0 :             toCancelEvent.delayId,
     211              :             DelayedEventAction.cancel,
     212              :           );
     213              :         }
     214              : 
     215              :         Map<String, List> newContent;
     216            0 :         if (roomVersion?.contains('msc3757') ?? false) {
     217              :           // scoped to deviceIds so clear the whole mems list
     218            0 :           newContent = {
     219            0 :             'memberships': [],
     220              :           };
     221              :         } else {
     222              :           // only clear our own deviceId
     223            0 :           final ownMemberships = getCallMembershipsForUser(
     224            0 :             client.userID!,
     225            0 :             client.deviceID!,
     226              :             voip,
     227              :           );
     228              : 
     229            0 :           ownMemberships.removeWhere(
     230            0 :             (mem) =>
     231            0 :                 mem.callId == groupCallId &&
     232            0 :                 mem.deviceId == client.deviceID! &&
     233            0 :                 mem.application == application &&
     234            0 :                 mem.scope == scope,
     235              :           );
     236              : 
     237            0 :           newContent = {
     238            0 :             'memberships': List.from(ownMemberships.map((e) => e.toJson())),
     239              :           };
     240              :         }
     241              : 
     242            0 :         _delayedLeaveEventId = await client.setRoomStateWithKeyWithDelay(
     243            0 :           id,
     244              :           EventTypes.GroupCallMember,
     245              :           stateKey,
     246            0 :           voip.timeouts!.delayedEventApplyLeave.inMilliseconds,
     247              :           newContent,
     248              :         );
     249              : 
     250            0 :         _restartDelayedLeaveEventTimer = Timer.periodic(
     251            0 :           voip.timeouts!.delayedEventRestart,
     252            0 :           ((timer) async {
     253            0 :             Logs()
     254            0 :                 .v('[_restartDelayedLeaveEventTimer] heartbeat delayed event');
     255            0 :             await client.manageDelayedEvent(
     256              :               _delayedLeaveEventId!,
     257              :               DelayedEventAction.restart,
     258              :             );
     259              :           }),
     260              :         );
     261              :       }
     262              : 
     263            8 :       return await client.setRoomStateWithKey(
     264            4 :         id,
     265              :         EventTypes.GroupCallMember,
     266              :         stateKey,
     267              :         newContent,
     268              :       );
     269              :     } else {
     270            0 :       throw MatrixSDKVoipException(
     271              :         '''
     272            0 :         User ${client.userID}:${client.deviceID} is not allowed to join famedly calls in room $id,
     273            0 :         canJoinGroupCall: $canJoinGroupCall,
     274            0 :         groupCallsEnabledForEveryone: $groupCallsEnabledForEveryone,
     275            0 :         needed: ${powerForChangingStateEvent(EventTypes.GroupCallMember)},
     276            0 :         own: $ownPowerLevel}
     277            0 :         plMap: ${getState(EventTypes.RoomPowerLevels)?.content}
     278            0 :         ''',
     279              :       );
     280              :     }
     281              :   }
     282              : 
     283              :   /// returns a list of memberships from a famedly call matrix event
     284            6 :   List<CallMembership> getCallMembershipsFromEvent(
     285              :     MatrixEvent event,
     286              :     VoIP voip,
     287              :   ) {
     288           18 :     if (event.roomId != id) return [];
     289            6 :     return getCallMembershipsFromEventContent(
     290            6 :       event.content,
     291            6 :       event.senderId,
     292            6 :       event.roomId!,
     293            6 :       event.eventId,
     294              :       voip,
     295              :     );
     296              :   }
     297              : 
     298              :   /// returns a list of memberships from a famedly call matrix event
     299            6 :   List<CallMembership> getCallMembershipsFromEventContent(
     300              :     Map<String, Object?> content,
     301              :     String senderId,
     302              :     String roomId,
     303              :     String? eventId,
     304              :     VoIP voip,
     305              :   ) {
     306            6 :     final mems = content.tryGetList<Map>('memberships');
     307            6 :     final callMems = <CallMembership>[];
     308           12 :     for (final m in mems ?? []) {
     309            6 :       final mem = CallMembership.fromJson(m, senderId, roomId, eventId, voip);
     310            6 :       if (mem != null) callMems.add(mem);
     311              :     }
     312              :     return callMems;
     313              :   }
     314              : }
     315              : 
     316            6 : bool isValidMemEvent(Map<String, Object?> event) {
     317           12 :   if (event['call_id'] is String &&
     318           12 :       event['device_id'] is String &&
     319           12 :       event['expires_ts'] is num &&
     320           12 :       event['foci_active'] is List) {
     321              :     return true;
     322              :   } else {
     323            0 :     Logs()
     324            0 :         .v('[VOIP] FamedlyCallMemberEvent ignoring unclean membership $event');
     325              :     return false;
     326              :   }
     327              : }
     328              : 
     329              : class MatrixSDKVoipException implements Exception {
     330              :   final String cause;
     331              :   final StackTrace? stackTrace;
     332              : 
     333            0 :   MatrixSDKVoipException(this.cause, {this.stackTrace});
     334              : 
     335            0 :   @override
     336            0 :   String toString() => '[VOIP] $cause, ${super.toString()}, $stackTrace';
     337              : }
        

Generated by: LCOV version 2.0-1