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 : }
|