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, 84 : ) async { 85 0 : final ownMemberships = getCallMembershipsForUser(client.userID!); 86 : 87 : // do not bother removing other deviceId expired events because we have no 88 : // ownership over them 89 : ownMemberships 90 0 : .removeWhere((element) => client.deviceID! == element.deviceId); 91 : 92 0 : ownMemberships.removeWhere((e) => e == callMembership); 93 : 94 0 : ownMemberships.add(callMembership); 95 : 96 0 : final newContent = { 97 0 : 'memberships': List.from(ownMemberships.map((e) => e.toJson())), 98 : }; 99 : 100 0 : await setFamedlyCallMemberEvent(newContent); 101 : } 102 : 103 0 : Future<void> removeFamedlyCallMemberEvent( 104 : String groupCallId, 105 : String deviceId, { 106 : String? application = 'm.call', 107 : String? scope = 'm.room', 108 : }) async { 109 0 : final ownMemberships = getCallMembershipsForUser(client.userID!); 110 : 111 0 : ownMemberships.removeWhere( 112 0 : (mem) => 113 0 : mem.callId == groupCallId && 114 0 : mem.deviceId == deviceId && 115 0 : mem.application == application && 116 0 : mem.scope == scope, 117 : ); 118 : 119 0 : final newContent = { 120 0 : 'memberships': List.from(ownMemberships.map((e) => e.toJson())), 121 : }; 122 0 : await setFamedlyCallMemberEvent(newContent); 123 : } 124 : 125 0 : Future<void> setFamedlyCallMemberEvent(Map<String, List> newContent) async { 126 0 : if (canJoinGroupCall) { 127 0 : await client.setRoomStateWithKey( 128 0 : id, 129 : EventTypes.GroupCallMember, 130 0 : client.userID!, 131 : newContent, 132 : ); 133 : } else { 134 0 : throw MatrixSDKVoipException( 135 : ''' 136 0 : User ${client.userID}:${client.deviceID} is not allowed to join famedly calls in room $id, 137 0 : canJoinGroupCall: $canJoinGroupCall, 138 0 : groupCallsEnabledForEveryone: $groupCallsEnabledForEveryone, 139 0 : needed: ${powerForChangingStateEvent(EventTypes.GroupCallMember)}, 140 0 : own: $ownPowerLevel} 141 0 : plMap: ${getState(EventTypes.RoomPowerLevels)?.content} 142 0 : ''', 143 : ); 144 : } 145 : } 146 : 147 : /// returns a list of memberships from a famedly call matrix event 148 2 : List<CallMembership> getCallMembershipsFromEvent(MatrixEvent event) { 149 6 : if (event.roomId != id) return []; 150 2 : return getCallMembershipsFromEventContent( 151 2 : event.content, 152 2 : event.senderId, 153 2 : event.roomId!, 154 : ); 155 : } 156 : 157 : /// returns a list of memberships from a famedly call matrix event 158 2 : List<CallMembership> getCallMembershipsFromEventContent( 159 : Map<String, Object?> content, 160 : String senderId, 161 : String roomId, 162 : ) { 163 2 : final mems = content.tryGetList<Map>('memberships'); 164 2 : final callMems = <CallMembership>[]; 165 4 : for (final m in mems ?? []) { 166 2 : final mem = CallMembership.fromJson(m, senderId, roomId); 167 2 : if (mem != null) callMems.add(mem); 168 : } 169 : return callMems; 170 : } 171 : } 172 : 173 2 : bool isValidMemEvent(Map<String, Object?> event) { 174 4 : if (event['call_id'] is String && 175 4 : event['device_id'] is String && 176 4 : event['expires_ts'] is num && 177 4 : event['foci_active'] is List) { 178 : return true; 179 : } else { 180 0 : Logs() 181 0 : .v('[VOIP] FamedlyCallMemberEvent ignoring unclean membership $event'); 182 : return false; 183 : } 184 : } 185 : 186 : class MatrixSDKVoipException implements Exception { 187 : final String cause; 188 : final StackTrace? stackTrace; 189 : 190 0 : MatrixSDKVoipException(this.cause, {this.stackTrace}); 191 : 192 0 : @override 193 0 : String toString() => '[VOIP] $cause, ${super.toString()}, $stackTrace'; 194 : }