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