LCOV - code coverage report
Current view: top level - lib/cwtch - cwtchNotifier.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 352 0.0 %
Date: 2026-09-01 20:55:37 Functions: 0 0 -

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

Generated by: LCOV version 1.14