169 lines
6.4 KiB
Dart
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();
|
|
}
|
|
}
|