Saba-dart/lib/utils/message_processor.dart
2026-04-13 23:41:27 +03:30

169 lines
6.4 KiB
Dart

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<ChatModel> processSmsList(
List<Map<String, dynamic>> rawSmsList, String targetAddress) {
// 1. Sort by date
final allSms = List<Map<String, dynamic>>.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 = <ChatModel>[];
final processedPacketIds = <String>{};
final packetLeaders = <String, int>{}; // packetKey -> newest SMS ID
final packetFragments = <String, List<Map<String, dynamic>>>{};
final packetTotals = <String, int>{};
final allDecryptedBodies = <String>{};
// 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();
}
}