Line data Source code
1 : import 'package:collection/collection.dart'; 2 : 3 : import 'package:matrix/matrix.dart'; 4 : import 'package:matrix/src/voip/models/call_membership.dart'; 5 : 6 : extension FamedlyCallMemberEventsExtension on Room { 7 : /// a map of every users famedly call event, holds the memberships list 8 : /// returns sorted according to originTs (oldest to newest) 9 2 : Map<String, FamedlyCallMemberEvent> getFamedlyCallEvents() { 10 2 : final Map<String, FamedlyCallMemberEvent> mappedEvents = {}; 11 : final famedlyCallMemberStates = 12 4 : states.tryGetMap<String, Event>(EventTypes.GroupCallMember); 13 : 14 2 : if (famedlyCallMemberStates == null) return {}; 15 2 : final sortedEvents = famedlyCallMemberStates.values 16 10 : .sorted((a, b) => a.originServerTs.compareTo(b.originServerTs)); 17 : 18 4 : for (final element in sortedEvents) { 19 : mappedEvents 20 8 : .addAll({element.senderId: FamedlyCallMemberEvent.fromJson(element)}); 21 : } 22 : return mappedEvents; 23 : } 24 : 25 : /// extracts memberships list form a famedly call event and maps it to a userid 26 : /// returns sorted (oldest to newest) 27 2 : Map<String, List<CallMembership>> getCallMembershipsFromRoom() { 28 2 : final parsedMemberEvents = getFamedlyCallEvents(); 29 2 : final Map<String, List<CallMembership>> memberships = {}; 30 4 : for (final element in parsedMemberEvents.entries) { 31 10 : memberships.addAll({element.key: element.value.memberships}); 32 : } 33 : return memberships; 34 : } 35 : 36 : /// returns a list of memberships in the room for `user` 37 0 : List<CallMembership> getCallMembershipsForUser(String userId) { 38 0 : final parsedMemberEvents = getCallMembershipsFromRoom(); 39 0 : final mem = parsedMemberEvents.tryGet<List<CallMembership>>(userId); 40 0 : return mem ?? []; 41 : } 42 : 43 : /// returns the user count (not sessions, yet) for the group call with id: `groupCallId`. 44 : /// returns 0 if group call not found 45 2 : int groupCallParticipantCount(String groupCallId) { 46 : int participantCount = 0; 47 : // userid:membership 48 2 : final memberships = getCallMembershipsFromRoom(); 49 : 50 4 : memberships.forEach((key, value) { 51 4 : for (final membership in value) { 52 6 : if (membership.callId == groupCallId && !membership.isExpired) { 53 2 : participantCount++; 54 : } 55 : } 56 : }); 57 : 58 : return participantCount; 59 : } 60 : 61 2 : bool get hasActiveGroupCall { 62 4 : if (activeGroupCallIds.isNotEmpty) { 63 : return true; 64 : } 65 : return false; 66 : } 67 : 68 : /// list of active group call ids 69 2 : List<String> get activeGroupCallIds { 70 : final Set<String> ids = {}; 71 2 : final memberships = getCallMembershipsFromRoom(); 72 : 73 4 : memberships.forEach((key, value) { 74 4 : for (final mem in value) { 75 6 : if (!mem.isExpired) ids.add(mem.callId); 76 : } 77 : }); 78 2 : return ids.toList(); 79 : } 80 : 81 : /// passing no `CallMembership` removes it from the state event. 82 0 : Future<void> updateFamedlyCallMemberStateEvent( 83 : CallMembership callMembership) async { 84 0 : final ownMemberships = getCallMembershipsForUser(client.userID!); 85 : 86 : // do not bother removing other deviceId expired events because we have no 87 : // ownership over them 88 : ownMemberships 89 0 : .removeWhere((element) => client.deviceID! == element.deviceId); 90 : 91 0 : ownMemberships.removeWhere((e) => e == callMembership); 92 : 93 0 : ownMemberships.add(callMembership); 94 : 95 0 : final newContent = { 96 0 : 'memberships': List.from(ownMemberships.map((e) => e.toJson())) 97 : }; 98 : 99 0 : await setFamedlyCallMemberEvent(newContent); 100 : } 101 : 102 0 : Future<void> removeFamedlyCallMemberEvent( 103 : String groupCallId, 104 : String deviceId, { 105 : String? application = 'm.call', 106 : String? scope = 'm.room', 107 : }) async { 108 0 : final ownMemberships = getCallMembershipsForUser(client.userID!); 109 : 110 0 : ownMemberships.removeWhere((mem) => 111 0 : mem.callId == groupCallId && 112 0 : mem.deviceId == deviceId && 113 0 : mem.application == application && 114 0 : mem.scope == scope); 115 : 116 0 : final newContent = { 117 0 : 'memberships': List.from(ownMemberships.map((e) => e.toJson())) 118 : }; 119 0 : await setFamedlyCallMemberEvent(newContent); 120 : } 121 : 122 0 : Future<void> setFamedlyCallMemberEvent(Map<String, List> newContent) async { 123 0 : if (canJoinGroupCall) { 124 0 : await client.setRoomStateWithKey( 125 0 : id, 126 : EventTypes.GroupCallMember, 127 0 : client.userID!, 128 : newContent, 129 : ); 130 : } else { 131 0 : throw MatrixSDKVoipException( 132 : ''' 133 0 : User ${client.userID}:${client.deviceID} is not allowed to join famedly calls in room $id, 134 0 : canJoinGroupCall: $canJoinGroupCall, 135 0 : groupCallsEnabledForEveryone: $groupCallsEnabledForEveryone, 136 0 : needed: ${powerForChangingStateEvent(EventTypes.GroupCallMember)}, 137 0 : own: $ownPowerLevel} 138 0 : plMap: ${getState(EventTypes.RoomPowerLevels)?.content} 139 0 : ''', 140 : ); 141 : } 142 : } 143 : 144 : /// returns a list of memberships from a famedly call matrix event 145 2 : List<CallMembership> getCallMembershipsFromEvent(MatrixEvent event) { 146 6 : if (event.roomId != id) return []; 147 2 : return getCallMembershipsFromEventContent( 148 6 : event.content, event.senderId, event.roomId!); 149 : } 150 : 151 : /// returns a list of memberships from a famedly call matrix event 152 2 : List<CallMembership> getCallMembershipsFromEventContent( 153 : Map<String, Object?> content, String senderId, String roomId) { 154 2 : final mems = content.tryGetList<Map>('memberships'); 155 2 : final callMems = <CallMembership>[]; 156 4 : for (final m in mems ?? []) { 157 2 : final mem = CallMembership.fromJson(m, senderId, roomId); 158 2 : if (mem != null) callMems.add(mem); 159 : } 160 : return callMems; 161 : } 162 : } 163 : 164 2 : bool isValidMemEvent(Map<String, Object?> event) { 165 4 : if (event['call_id'] is String && 166 4 : event['device_id'] is String && 167 4 : event['expires_ts'] is num && 168 4 : event['foci_active'] is List) { 169 : return true; 170 : } else { 171 0 : Logs() 172 0 : .v('[VOIP] FamedlyCallMemberEvent ignoring unclean membership $event'); 173 : return false; 174 : } 175 : } 176 : 177 : class MatrixSDKVoipException implements Exception { 178 : final String cause; 179 : final StackTrace? stackTrace; 180 : 181 0 : MatrixSDKVoipException(this.cause, {this.stackTrace}); 182 : 183 0 : @override 184 0 : String toString() => '[VOIP] $cause, ${super.toString()}, $stackTrace'; 185 : }