Line data Source code
1 : import 'dart:convert';
2 : import 'package:cwtch/cwtch/cwtch.dart';
3 : import 'package:cwtch/main.dart';
4 : import 'package:cwtch/models/appstate.dart';
5 : import 'package:cwtch/models/contact.dart';
6 : import 'package:cwtch/models/groupmembers.dart';
7 : import 'package:cwtch/models/hybridgroups.dart';
8 : import 'package:cwtch/models/profilelist.dart';
9 : import 'package:cwtch/models/remoteserver.dart';
10 : import 'package:cwtch/models/search.dart';
11 : import 'package:cwtch/models/servers.dart';
12 : import 'package:cwtch/notification_manager.dart';
13 :
14 : import 'package:cwtch/torstatus.dart';
15 :
16 : import '../config.dart';
17 : import '../errorHandler.dart';
18 : import '../settings.dart';
19 :
20 : typedef SeenMessageCallback = Function(String, int, DateTime);
21 :
22 : // Class that handles libcwtch-go events (received either via ffi with an isolate or gomobile over a method channel from kotlin)
23 : // Takes Notifiers and triggers them on appropriate events
24 : class CwtchNotifier {
25 : late ProfileListState profileCN;
26 : late Settings settings;
27 : late ErrorHandler error;
28 : late TorStatus torStatus;
29 : late NotificationsManager notificationManager;
30 : late AppState appState;
31 : late ServerListState serverListState;
32 : late HybridGroupsListState groupListState;
33 : late FlwtchState flwtchState;
34 :
35 : String? notificationSimple;
36 : String? notificationConversationInfo;
37 :
38 : SeenMessageCallback? seenMessageCallback;
39 :
40 0 : CwtchNotifier(
41 : ProfileListState pcn,
42 : Settings settingsCN,
43 : ErrorHandler errorCN,
44 : TorStatus torStatusCN,
45 : NotificationsManager notificationManagerP,
46 : AppState appStateCN,
47 : ServerListState serverListStateCN,
48 : HybridGroupsListState groupListStateCN,
49 : FlwtchState flwtchStateCN,
50 : ) {
51 0 : profileCN = pcn;
52 0 : settings = settingsCN;
53 0 : error = errorCN;
54 0 : torStatus = torStatusCN;
55 0 : notificationManager = notificationManagerP;
56 0 : appState = appStateCN;
57 0 : serverListState = serverListStateCN;
58 0 : groupListState = groupListStateCN;
59 0 : flwtchState = flwtchStateCN;
60 : }
61 :
62 0 : void l10nInit(String notificationSimple, String notificationConversationInfo) {
63 0 : this.notificationSimple = notificationSimple;
64 0 : this.notificationConversationInfo = notificationConversationInfo;
65 : }
66 :
67 0 : void setMessageSeenCallback(SeenMessageCallback callback) {
68 0 : seenMessageCallback = callback;
69 : }
70 :
71 0 : void handleMessage(String type, dynamic data) {
72 : // EnvironmentConfig.debugLog("NewEvent $type $data");
73 : switch (type) {
74 0 : case "CwtchStarted":
75 0 : if (data["Reload"] == "true" && profileCN.num > 0) {
76 : // don't reload...
77 : // unless we have loaded no profiles...then there isnt a risk and this
78 : // might be a first time (e.g. new apk, existing service)
79 : } else {
80 0 : flwtchState.cwtch.LoadProfiles(DefaultPassword);
81 : }
82 :
83 0 : appState.SetCwtchInit();
84 : break;
85 0 : case "CwtchStartError":
86 0 : appState.SetAppError(data["Error"]);
87 : break;
88 0 : case "NewPeer":
89 : // autobindings is forwarding unenriched NewPeer events for hybrid groups
90 : // we should find & fix this at the source rather than here.
91 0 : if (data["tag"] == "v1-managedGroup" || !data.containsKey('name')) break;
92 :
93 : // else if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta...
94 0 : profileCN.add(
95 0 : data["Identity"],
96 0 : data["name"],
97 0 : data["private-name"],
98 0 : data["picture"],
99 0 : data["defaultPicture"],
100 0 : data["ContactsJson"],
101 0 : data["ServerList"],
102 0 : data["Online"] == "true",
103 0 : data["autostart"] == "true",
104 0 : data["tag"] != "v1-defaultPassword",
105 0 : data["appearOffline"] == "true",
106 : );
107 :
108 : // Update Profile Attributes
109 0 : flwtchState.cwtch.GetProfileAttribute(data["Identity"], "profile.profile-attribute-1").then((value) => profileCN.getProfile(data["Identity"])?.setAttribute(0, value));
110 0 : flwtchState.cwtch.GetProfileAttribute(data["Identity"], "profile.profile-attribute-2").then((value) => profileCN.getProfile(data["Identity"])?.setAttribute(1, value));
111 0 : flwtchState.cwtch.GetProfileAttribute(data["Identity"], "profile.profile-attribute-3").then((value) => profileCN.getProfile(data["Identity"])?.setAttribute(2, value));
112 0 : flwtchState.cwtch.GetProfileAttribute(data["Identity"], "profile.profile-status").then((value) => profileCN.getProfile(data["Identity"])?.setAvailabilityStatus(value ?? ""));
113 :
114 0 : profileCN.getProfile(data["Identity"])?.contactList.contacts.forEach((contact) {
115 0 : flwtchState.cwtch.GetConversationAttribute(data["Identity"], contact.identifier, "public.profile.profile-attribute-1").then((value) => contact.setAttribute(0, value));
116 0 : flwtchState.cwtch.GetConversationAttribute(data["Identity"], contact.identifier, "public.profile.profile-attribute-2").then((value) => contact.setAttribute(1, value));
117 0 : flwtchState.cwtch.GetConversationAttribute(data["Identity"], contact.identifier, "public.profile.profile-attribute-3").then((value) => contact.setAttribute(2, value));
118 0 : flwtchState.cwtch.GetConversationAttribute(data["Identity"], contact.identifier, "public.profile.profile-status").then((value) => contact.setAvailabilityStatus(value ?? ""));
119 : });
120 :
121 : break;
122 :
123 0 : case "ContactCreated":
124 0 : var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(int.parse(data["ConversationID"]));
125 : if (contact != null) {
126 0 : contact.accepted = (data["accepted"] == "true");
127 0 : contact.isShadowed = (data["Shadowed"] == "true");
128 : } else {
129 0 : profileCN
130 0 : .getProfile(data["ProfileOnion"])
131 0 : ?.contactList
132 0 : .add(
133 0 : ContactInfoState(
134 0 : data["ProfileOnion"],
135 0 : int.parse(data["ConversationID"]),
136 0 : data["RemotePeer"],
137 0 : nickname: data["nick"],
138 0 : status: data["status"],
139 0 : imagePath: data["picture"],
140 0 : defaultImagePath: data["defaultPicture"],
141 0 : blocked: data["blocked"] == "true",
142 0 : accepted: data["accepted"] == "true",
143 0 : savePeerHistory: data["saveConversationHistory"] == null ? "DeleteHistoryConfirmed" : data["saveConversationHistory"],
144 0 : numMessages: int.parse(data["numMessages"]),
145 0 : numUnread: int.parse(data["unread"]),
146 : isGroup: false, // by definition
147 : server: null,
148 : archived: false,
149 0 : lastMessageTime: DateTime.now(), //show at the top of the contact list even if no messages yet
150 0 : notificationPolicy: data["notificationPolicy"] ?? "ConversationNotificationPolicy.Default",
151 0 : isManaged: data["managed"] == "true",
152 : ),
153 : );
154 0 : contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
155 0 : contact!.isShadowed = data["Shadowed"] == "true";
156 : }
157 :
158 : break;
159 0 : case "NewServer":
160 0 : serverListState.add(data["Onion"], data["ServerBundle"], data["Running"] == "true", data["Description"], data["Autostart"] == "true", data["StorageType"] == "storage-password");
161 : break;
162 0 : case "ServerIntentUpdate":
163 0 : var server = serverListState.getServer(data["Identity"]);
164 : if (server != null) {
165 0 : server.setRunning(data["Intent"] == "running");
166 : }
167 : break;
168 0 : case "ServerStatsUpdate":
169 0 : EnvironmentConfig.debugLog("ServerStatsUpdate $data");
170 0 : var totalMessages = int.parse(data["TotalMessages"]);
171 0 : var connections = int.parse(data["Connections"]);
172 0 : serverListState.updateServerStats(data["Identity"], totalMessages, connections);
173 : break;
174 0 : case "GroupCreated":
175 : // Retrieve Server Status from Cache...
176 : String status = "";
177 0 : RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]);
178 : if (serverInfoState != null) {
179 0 : status = serverInfoState.status;
180 : }
181 0 : if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(int.parse(data["ConversationID"])) == null) {
182 0 : profileCN
183 0 : .getProfile(data["ProfileOnion"])
184 0 : ?.contactList
185 0 : .add(
186 0 : ContactInfoState(
187 0 : data["ProfileOnion"],
188 0 : int.parse(data["ConversationID"]),
189 0 : data["GroupID"],
190 : blocked: false, // we created
191 : accepted: true, // we created
192 0 : imagePath: data["picture"],
193 0 : defaultImagePath: data["picture"],
194 0 : nickname: data["GroupName"],
195 : status: status,
196 0 : server: data["GroupServer"],
197 : isGroup: true,
198 0 : lastMessageTime: DateTime.now(),
199 0 : notificationPolicy: data["notificationPolicy"] ?? "ConversationNotificationPolicy.Default",
200 : ),
201 : );
202 :
203 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageReceivedTime(int.parse(data["ConversationID"]), DateTime.now());
204 : }
205 : break;
206 0 : case "PeerDeleted":
207 0 : profileCN.delete(data["Identity"]);
208 : // todo standarize
209 0 : error.handleUpdate("deleteprofile.success");
210 : break;
211 0 : case "ServerDeleted":
212 0 : error.handleUpdate("deletedserver." + data["Status"]);
213 0 : if (data["Status"] == "success") {
214 0 : serverListState.delete(data["Identity"]);
215 : }
216 : break;
217 0 : case "DeleteContact":
218 0 : var identifier = int.parse(data["ConversationID"]);
219 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(identifier);
220 : break;
221 0 : case "PeerStateChange":
222 0 : ContactInfoState? contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
223 : if (contact != null) {
224 0 : if (data["ConnectionState"] != null) {
225 0 : contact.status = data["ConnectionState"];
226 : }
227 0 : profileCN.getProfile(data["ProfileOnion"])?.resortContacts();
228 : }
229 : break;
230 0 : case "NewMessageFromPeer":
231 0 : var identifier = int.parse(data["ConversationID"]);
232 0 : var messageID = int.parse(data["Index"]);
233 0 : var timestamp = DateTime.tryParse(data['TimestampReceived'])!;
234 0 : var senderHandle = data['RemotePeer'];
235 0 : var senderImage = data['picture'];
236 0 : var isAuto = data['Auto'] == "true";
237 0 : String contenthash = data['ContentHash'];
238 :
239 : try {
240 0 : dynamic message = jsonDecode(data["Data"]);
241 0 : var overlay = int.parse(message['o'].toString());
242 0 : if (overlay > 1024 && overlay & 0x07 != 0) {
243 : break;
244 : }
245 : } catch (e) {
246 : // malformed message...
247 : }
248 :
249 0 : var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
250 0 : var selectedConversation = selectedProfile && appState.selectedConversation == identifier;
251 0 : profileCN
252 0 : .getProfile(data["ProfileOnion"])
253 0 : ?.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedProfile, selectedConversation, "");
254 :
255 : // Now perform the notification logic...
256 0 : var notification = data["notification"];
257 0 : if (selectedConversation && seenMessageCallback != null) {
258 0 : seenMessageCallback!(data["ProfileOnion"]!, identifier, DateTime.now().toUtc());
259 : }
260 :
261 0 : if (notification == "SimpleEvent") {
262 0 : notificationManager.notify(notificationSimple ?? "New Message", "", 0);
263 0 : } else if (notification == "ContactInfo") {
264 0 : var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
265 0 : notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString())), data["ProfileOnion"], identifier);
266 : }
267 0 : appState.notifyProfileUnread();
268 : break;
269 0 : case "PeerAcknowledgement":
270 : // We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end...
271 : break;
272 0 : case "IndexedAcknowledgement":
273 0 : var conversation = int.parse(data["ConversationID"]);
274 0 : var messageID = int.parse(data["Index"]);
275 :
276 : // We only ever see acks from authenticated peers.
277 : // If the contact is marked as offline then override this - can happen when the contact is removed from the front
278 : // end during syncing.
279 0 : if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.isOnline() == false) {
280 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.status = "Authenticated";
281 : }
282 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.ackCache(messageID);
283 :
284 : break;
285 0 : case "NewMessageFromGroup":
286 0 : var identifier = int.parse(data["ConversationID"]);
287 0 : if (data["ProfileOnion"] != data["RemotePeer"]) {
288 0 : var idx = int.parse(data["Index"]);
289 0 : var senderHandle = data['RemotePeer'];
290 0 : var senderImage = data['picture'];
291 0 : var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
292 0 : var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
293 0 : var currentTotal = contact!.totalMessages;
294 0 : var isAuto = data['Auto'] == "true";
295 0 : String contenthash = data['ContentHash'];
296 0 : var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
297 0 : var selectedConversation = selectedProfile && appState.selectedConversation == identifier;
298 0 : var notification = data["notification"];
299 0 : var signature = data["Signature"] ?? "";
300 :
301 : // Only bother to do anything if we know about the group and the provided index is greater than our current total...
302 0 : if (idx >= currentTotal) {
303 : // TODO: There are 2 timestamps associated with a new group message - time sent and time received.
304 : // Sent refers to the time a profile alleges they sent a message
305 : // Received refers to the time we actually saw the message from the server
306 : // These can obviously be very different for legitimate reasons.
307 : // We also maintain a relative hash-link through PreviousMessageSignature which is the ground truth for
308 : // order.
309 : // In the future we will want to combine these 3 ordering mechanisms into a cohesive view of the timeline
310 : // For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts
311 : // and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time`
312 : // and `local now`.
313 0 : profileCN
314 0 : .getProfile(data["ProfileOnion"])
315 0 : ?.newMessage(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedProfile, selectedConversation, signature);
316 0 : if (selectedConversation && seenMessageCallback != null) {
317 0 : seenMessageCallback!(data["ProfileOnion"]!, identifier, DateTime.now().toUtc());
318 : }
319 :
320 0 : if (notification == "SimpleEvent") {
321 0 : notificationManager.notify(notificationSimple ?? "New Message", "", 0);
322 0 : } else if (notification == "ContactInfo") {
323 0 : var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
324 0 : notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString())), data["ProfileOnion"], identifier);
325 : }
326 0 : appState.notifyProfileUnread();
327 : }
328 0 : RemoteServerInfoState? server = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(contact.server ?? "");
329 0 : server?.updateSyncProgressFor(timestampSent);
330 : } else {
331 : // This is dealt with by IndexedAcknowledgment
332 0 : EnvironmentConfig.debugLog("new message from group from yourself - this should not happen");
333 : }
334 : break;
335 0 : case "IndexedFailure":
336 0 : var identifier = int.parse(data["ConversationID"]);
337 0 : var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
338 0 : var messageID = int.parse(data["Index"]);
339 0 : contact?.errCache(messageID);
340 : break;
341 0 : case "AppError":
342 0 : EnvironmentConfig.debugLog("New App Error: $data");
343 : // special case for delete error (todo: standardize cwtch errors)
344 0 : if (data["Error"] == "Password did not match") {
345 0 : error.handleUpdate("deleteprofile.error");
346 0 : } else if (data["Data"] != null) {
347 0 : error.handleUpdate(data["Data"]);
348 : }
349 : break;
350 0 : case "UpdateGlobalSettings":
351 0 : settings.handleUpdate(jsonDecode(data["Data"]));
352 0 : appState.settingsLoaded = true;
353 : break;
354 0 : case "UpdatedProfileAttribute":
355 0 : if (data["Key"] == "public.profile.name") {
356 0 : profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"];
357 0 : } else if (data["Key"].toString().startsWith("local.filesharing.")) {
358 0 : if (data["Key"].toString().endsWith(".path")) {
359 : // local.conversation.filekey.path
360 0 : List<String> keyparts = data["Key"].toString().split(".");
361 0 : if (keyparts.length == 5) {
362 0 : String filekey = keyparts[2] + "." + keyparts[3];
363 0 : profileCN.getProfile(data["ProfileOnion"])?.downloadSetPathForSender(filekey, data["Data"]);
364 : }
365 : }
366 0 : } else if (data["Key"].toString().startsWith("local.profile.private-name")) {
367 0 : profileCN.getProfile(data["ProfileOnion"])?.setPrivateName(data["Data"]);
368 0 : } else if (data["Key"].toString().startsWith("public.profile.profile-attribute")) {
369 : // ignore these events...
370 0 : } else if (data["Key"].toString().startsWith("public.profile.profile-status")) {
371 0 : profileCN.getProfile(data["ProfileOnion"])?.setAvailabilityStatus(data["Data"]);
372 : } else {
373 0 : EnvironmentConfig.debugLog("unhandled set attribute event: ${data['Key']}");
374 : }
375 : break;
376 0 : case "NetworkError":
377 0 : var isOnline = data["Status"] == "Success";
378 0 : profileCN.getProfile(data["ProfileOnion"])?.isOnline = isOnline;
379 : break;
380 0 : case "ACNStatus":
381 0 : EnvironmentConfig.debugLog("acn status: $data");
382 0 : torStatus.handleUpdate(int.parse(data["Progress"]), data["Status"]);
383 : break;
384 0 : case "ACNVersion":
385 0 : EnvironmentConfig.debugLog("acn version: $data");
386 0 : torStatus.updateVersion(data["Data"]);
387 : break;
388 0 : case "UpdateServerInfo":
389 0 : EnvironmentConfig.debugLog("NewEvent UpdateServerInfo $type $data");
390 0 : profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]);
391 : break;
392 0 : case "TokenManagerInfo":
393 : try {
394 0 : List<dynamic> associatedGroups = jsonDecode(data["Data"]);
395 0 : int count = int.parse(data["ServerTokenCount"]);
396 0 : associatedGroups.forEach((identifier) {
397 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(int.parse(identifier.toString()))!.antispamTickets = count;
398 : });
399 0 : EnvironmentConfig.debugLog("update server token count for $associatedGroups, $count");
400 : } catch (e) {
401 : // No tokens in data...
402 : }
403 : break;
404 0 : case "NewGroup":
405 0 : String invite = data["GroupInvite"].toString();
406 0 : if (invite.startsWith("torv3")) {
407 0 : String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5)));
408 0 : dynamic groupInvite = jsonDecode(inviteJson);
409 :
410 : // Retrieve Server Status from Cache...
411 : String status = "";
412 0 : RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])!.serverList.getServer(groupInvite["ServerHost"]);
413 : if (serverInfoState != null) {
414 0 : status = serverInfoState.status;
415 : }
416 :
417 0 : if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(groupInvite["GroupID"]) == null) {
418 0 : var identifier = int.parse(data["ConversationID"]);
419 0 : profileCN
420 0 : .getProfile(data["ProfileOnion"])
421 0 : ?.contactList
422 0 : .add(
423 0 : ContactInfoState(
424 0 : data["ProfileOnion"],
425 : identifier,
426 0 : groupInvite["GroupID"],
427 : blocked: false, // NewGroup only issued on accepting invite
428 : accepted: true, // NewGroup only issued on accepting invite
429 0 : imagePath: data["picture"],
430 0 : nickname: groupInvite["GroupName"],
431 0 : server: groupInvite["ServerHost"],
432 : status: status,
433 : isGroup: true,
434 0 : lastMessageTime: DateTime.now(),
435 : ),
436 : );
437 :
438 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageReceivedTime(identifier, DateTime.fromMillisecondsSinceEpoch(0));
439 : }
440 : // request a new server update...
441 : // NOTE: In the future this should also update the TokenManagerInfo
442 : // This is not currently communicated by ServerUpdateInfo (but it probably should be)
443 0 : flwtchState.cwtch.PublishServerUpdate(data["ProfileOnion"]);
444 : }
445 : break;
446 0 : case "ServerStateChange":
447 : // Update the Server Cache
448 0 : profileCN.getProfile(data["ProfileOnion"])?.updateServerStatusCache(data["GroupServer"], data["ConnectionState"]);
449 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.contacts.forEach((contact) {
450 0 : if (contact.isGroup == true && contact.server == data["GroupServer"]) {
451 0 : contact.status = data["ConnectionState"];
452 : }
453 : });
454 0 : profileCN.getProfile(data["ProfileOnion"])?.resortContacts();
455 : break;
456 0 : case "UpdatedConversationAttribute":
457 0 : if (data["Path"] == "profile.name") {
458 0 : if (data["Data"].toString().trim().length > 0) {
459 : // Update locally on the UI...
460 0 : if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) {
461 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.nickname = data["Data"];
462 : }
463 : }
464 0 : } else if (data['Path'] == "profile.custom-profile-image") {
465 0 : EnvironmentConfig.debugLog("received ret val of custom profile image: $data");
466 0 : String fileKey = data['Data'];
467 0 : var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
468 : if (contact != null) {
469 0 : EnvironmentConfig.debugLog("waiting for download from $contact");
470 0 : profileCN.getProfile(data["ProfileOnion"])?.waitForDownloadComplete(contact.identifier, fileKey);
471 : }
472 0 : } else if (data['Path'] == "profile.profile-attribute-1" || data['Path'] == "profile.profile-attribute-2" || data['Path'] == "profile.profile-attribute-3") {
473 0 : var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
474 : if (contact != null) {
475 0 : switch (data['Path']) {
476 0 : case "profile.profile-attribute-1":
477 0 : contact.setAttribute(0, data["Data"]);
478 : break;
479 0 : case "profile.profile-attribute-2":
480 0 : contact.setAttribute(1, data["Data"]);
481 : break;
482 0 : case "profile.profile-attribute-3":
483 0 : contact.setAttribute(2, data["Data"]);
484 : break;
485 : }
486 : }
487 0 : } else if (data['Path'] == "profile.profile-status") {
488 0 : var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
489 : if (contact != null) {
490 0 : contact.setAvailabilityStatus(data['Data']);
491 : }
492 0 : } else if (data['Path'] == 'conversation.shadowed') {
493 0 : var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
494 : if (contact != null) {
495 0 : contact.isShadowed = data['Data'] == "true";
496 : }
497 0 : } else if (data['Path'] == 'filesharing.custom-profile-image-path') {
498 : //ignore
499 : } else {
500 0 : EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}");
501 : }
502 0 : var c = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
503 0 : if (c != null && c.isManaged) {
504 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])?.isOpPending = false;
505 : }
506 : break;
507 0 : case "ManifestSizeReceived":
508 0 : if (profileCN.getProfile(data["ProfileOnion"]) == null) {
509 : break;
510 : }
511 0 : if (!profileCN.getProfile(data["ProfileOnion"])!.downloadActive(data["FileKey"])) {
512 0 : profileCN.getProfile(data["ProfileOnion"])?.downloadUpdate(data["FileKey"], 0, 1);
513 : }
514 : break;
515 0 : case "ManifestSaved":
516 0 : profileCN.getProfile(data["ProfileOnion"])?.downloadMarkManifest(data["FileKey"]);
517 : break;
518 0 : case "FileDownloadProgressUpdate":
519 0 : var progress = int.parse(data["Progress"]);
520 0 : profileCN.getProfile(data["ProfileOnion"])?.downloadUpdate(data["FileKey"], progress, int.parse(data["FileSizeInChunks"]));
521 : // progress == -1 is a "download was interrupted" message and should contain a path
522 0 : if (progress < 0) {
523 0 : profileCN.getProfile(data["ProfileOnion"])?.downloadSetPath(data["FileKey"], data["FilePath"]);
524 : }
525 : break;
526 0 : case "FileDownloaded":
527 0 : profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]);
528 : break;
529 0 : case "ImportingProfileEvent":
530 : break;
531 0 : case "StartingStorageMigration":
532 0 : appState.SetModalState(ModalState.storageMigration);
533 : break;
534 0 : case "DoneStorageMigration":
535 0 : appState.SetModalState(ModalState.none);
536 : break;
537 0 : case "BlodeuweddSummary":
538 0 : var identifier = int.parse(data["ConversationID"]);
539 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)?.updateSummaryEvent(data["Summary"]);
540 : break;
541 0 : case "BlodeuweddTranslation":
542 0 : var identifier = int.parse(data["ConversationID"]);
543 0 : var mid = int.parse(data["Index"]);
544 0 : EnvironmentConfig.debugLog("received translation event: $identifier $mid $data");
545 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)?.updateTranslationEvent(mid, data["Translation"]);
546 : break;
547 0 : case "ACNInfo":
548 0 : var key = data["Key"];
549 0 : var handle = data["Handle"];
550 0 : if (key == "circuit") {
551 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(handle)?.acnCircuit = data["Data"];
552 : }
553 : break;
554 0 : case "SearchResult":
555 0 : String searchID = data["SearchID"];
556 0 : var conversationIdentifier = int.parse(data["ConversationID"]);
557 0 : var messageIndex = int.parse(data["RowIndex"]);
558 0 : profileCN.getProfile(data["ProfileOnion"])?.searchState.handleSearchResult(searchID, conversationIdentifier, messageIndex);
559 : break;
560 0 : case "NewHybridGroup":
561 0 : groupListState.add(data["Onion"], data["GroupBundle"], data["Running"] == "true", data["Description"], data["Autostart"] == "true", data["StorageType"] == "storage-password");
562 : break;
563 0 : case "HybridGroupDeleted":
564 0 : error.handleUpdate("deletedhybridgroup." + data["Status"]);
565 0 : if (data["Status"] == "success") {
566 0 : groupListState.delete(data["Identity"]);
567 : }
568 : break;
569 0 : case "HybridGroupStatsUpdate":
570 0 : var totalMessages = int.parse(data["TotalMessages"]);
571 0 : var connections = int.parse(data["Connections"]);
572 0 : groupListState.updateGroupStats(data["Identity"], totalMessages, connections);
573 : break;
574 0 : case "MemberList":
575 0 : dynamic message = jsonDecode(data["Data"]);
576 0 : var identifier = int.parse(data["ConversationID"]);
577 0 : List<GroupMember> members = [];
578 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)?.members = message["m"]
579 0 : .map<String, GroupMember>(
580 0 : (k, v) => MapEntry<String, GroupMember>(
581 : k,
582 0 : GroupMember(
583 : k, //v["Name"],
584 0 : v["AC"].cast<String, dynamic>(),
585 : //v["AC"].map<String,bool>((k,v)=>MapEntry<String,bool>(k, v as bool)) as Map<String,bool>
586 : ),
587 : ),
588 : )
589 0 : .values
590 0 : .toList();
591 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)?.modeLine = message["g"];
592 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)?.modeMask = message["h"];
593 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)?.isOpPending = false;
594 0 : profileCN.getProfile(data["ProfileOnion"])?.notifyListeners();
595 : break;
596 0 : case "MessageUpdated":
597 0 : var identifier = int.parse(data["ConversationID"]);
598 0 : var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
599 0 : var messageID = int.parse(data["Index"]);
600 0 : String? sig = data.containsKey("Signature") ? data["Signature"] : null;
601 0 : profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)?.isOpPending = false;
602 0 : contact?.updateMessage(messageID, data["Message"], sig, jsonDecode(data["Attributes"])); //.cast<String,String>());
603 : break;
604 : default:
605 0 : EnvironmentConfig.debugLog("unhandled event: $type");
606 : }
607 : }
608 : }
|