import '../models/chat_model.dart'; import 'protocol_helper.dart'; class MessageProcessor { /// Processes raw SMS data into ChatModel objects in a background-friendly way. /// This is intended to be called via compute() or Isolate.run(). static List processSmsList( List> rawSmsList, String targetAddress) { // 1. Sort by date final allSms = List>.from(rawSmsList) ..sort((a, b) { final dateComp = (a['date'] as int? ?? 0).compareTo(b['date'] as int? ?? 0); if (dateComp != 0) return dateComp; final smsIdComp = (a['sms_id'] as int? ?? 0).compareTo(b['sms_id'] as int? ?? 0); if (smsIdComp != 0) return smsIdComp; return (a['id'] as int? ?? 0).compareTo(b['id'] as int? ?? 0); }); final convertedMessages = []; final processedPacketIds = {}; final packetLeaders = {}; // packetKey -> newest SMS ID final packetFragments = >>{}; final packetTotals = {}; final allDecryptedBodies = {}; // PASS 1: Identify all packets and their status for (final sms in allSms) { final bodyText = (sms['body'] as String).trim(); final parsed = ProtocolHelper.parseMessage(bodyText); final type = parsed['type'] as String; final bool dbIsSecure = (sms['is_secure'] as int? ?? 0) == 1; final packetId = parsed['packetId'] as String?; final packetMode = (type == 'asym' || type == 'afrag') ? 'AE' : 'SYM'; final packetKey = (packetId == null || packetId.isEmpty) ? null : '$packetMode::$packetId'; if (type == 'sym' || type == 'asym') { if (packetKey != null) { processedPacketIds.add(packetKey); } } if (dbIsSecure && type == 'plain') { allDecryptedBodies.add(bodyText); } if (type == 'key_init' || type == 'key_reply' || type == 'norm') continue; if ((type == 'sfra' || type == 'afrag') && packetKey != null) { packetLeaders[packetKey] = sms['sms_id'] as int? ?? 0; packetFragments.putIfAbsent(packetKey, () => []).add(sms); packetTotals[packetKey] = parsed['totalParts'] as int? ?? 1; } } // PASS 2: Convert to ChatModel with linked deduplication for (final sms in allSms) { final isMe = (sms['is_me'] as int? ?? 0) == 1; final smsId = sms['sms_id'] as int?; final cacheRowId = sms['id'] as int?; final rawBody = (sms['body'] as String); final bool dbIsSecure = (sms['is_secure'] as int? ?? 0) == 1; final parsed = ProtocolHelper.parseMessage(rawBody); final type = parsed['type'] as String; // Deduplication Rule: If this is a plain message but its content is a known decrypted version of a secure message, skip. if (type == 'plain' && !dbIsSecure && allDecryptedBodies.contains(rawBody.trim())) { continue; } if (type == 'key_init' || type == 'key_reply' || type == 'norm') continue; final String? dbPacketId = sms['packet_id'] as String?; final String? dbPacketMode = sms['packet_mode'] as String?; final packetId = dbPacketId ?? parsed['packetId'] as String?; final bool isAsymmetric = type == 'afrag' || type == 'asym'; final String packetMode = dbPacketMode ?? (isAsymmetric ? 'AE' : 'SYM'); final packetKey = (packetId == null || packetId.isEmpty) ? null : '$packetMode::$packetId'; final isFragment = type == 'sfra' || type == 'afrag'; // Deduplication: If we already have a decrypted/complete version of this packet, skip the fragments. if (packetKey != null && processedPacketIds.contains(packetKey) && !dbIsSecure && (isFragment || type == 'sym' || type == 'asym')) { continue; } String bodyText = rawBody; String? encryptedPayload; bool isSecureMessage = dbIsSecure || type != 'plain'; bool isAssembled = false; if (isFragment && packetKey != null) { if (packetLeaders[packetKey] != sms['sms_id']) continue; final fragments = packetFragments[packetKey] ?? []; final totalPartsCount = packetTotals[packetKey] ?? 1; final receivedPartsCount = fragments.length; if (receivedPartsCount == totalPartsCount) { fragments.sort((a, b) { final pA = ProtocolHelper.parseMessage(a['body'] as String)['partNo'] as int? ?? 0; final pB = ProtocolHelper.parseMessage(b['body'] as String)['partNo'] as int? ?? 0; return pA.compareTo(pB); }); final payload = fragments .map((f) => ProtocolHelper.parseMessage(f['body'] as String)['chunk'] as String? ?? '') .join(); final isGroup = parsed['isGroup'] == true; bodyText = (type == 'sfra') ? (isGroup ? '${ProtocolHelper.gPrefix}${ProtocolHelper.typeSym}|$payload' : ProtocolHelper.buildSymmetricMsg(payload)) : ProtocolHelper.buildAsymmetricMsg(payload); encryptedPayload = payload; isAssembled = true; processedPacketIds.add(packetKey); } else { bodyText = 'در حال ${isMe ? "ارسال" : "دریافت"} قطعات... ($receivedPartsCount/$totalPartsCount)'; } } else if (type != 'plain') { if (type == 'sym' || type == 'asym') { encryptedPayload = parsed['payload'] as String?; if (packetKey != null) processedPacketIds.add(packetKey); } } convertedMessages.add(ChatModel( id: smsId, localId: cacheRowId != null ? 'cache::$cacheRowId' : null, body: bodyText, rawBody: rawBody, encryptedPayload: encryptedPayload, packetId: packetId, packetMode: (packetId != null) ? packetMode : null, date: sms['date'] as int? ?? 0, isMe: isMe, status: isMe ? MessageStatus.sent : MessageStatus.received, isSecure: isSecureMessage, isPendingMultipart: isFragment && !isAssembled, statusLabel: (isFragment && !isAssembled) ? 'در حال دریافت قطعات...' : null, )); } return convertedMessages.reversed.toList(); } }