Деdup: защита от дублей сообщений + Forward Picker UI parity

This commit is contained in:
2026-04-03 19:18:56 +05:00
parent da6b3d7c3f
commit 078e2d4feb
2 changed files with 37 additions and 3 deletions

View File

@@ -176,10 +176,21 @@ enum MessageCrypto {
var seen = Set<String>()
return candidates.filter { seen.insert($0).inserted }
}
// Group key or plain password try hex(utf8Bytes) first (Desktop parity:
// Desktop encrypts group attachments with Buffer.from(groupKey).toString('hex')).
// Group key or plain password Desktop encrypts group attachments with
// Buffer.from(groupKey).toString('hex') (hex of UTF-8 bytes).
// If stored is already hex-encoded (128+ chars, all hex digits), use as-is
// to avoid generating a 256-char double-hex garbage candidate.
let isAlreadyHex = stored.count >= 128 && stored.allSatisfy { $0.isHexDigit }
if isAlreadyHex {
// Try hex as-is (Desktop) + un-hexed original (iOS/Android plain group key)
let decoded = Data(hexString: stored)
if let original = String(data: decoded, encoding: .utf8), !original.isEmpty {
return [stored, original]
}
return [stored]
}
let hexVariant = Data(stored.utf8).map { String(format: "%02x", $0) }.joined()
return hexVariant == stored ? [stored] : [hexVariant, stored]
return [hexVariant, stored]
}
// MARK: - Android-Compatible UTF-8 Decoder

View File

@@ -1360,6 +1360,7 @@ final class SessionManager {
self.syncRequestInFlight = false
self.syncBatchInProgress = false
self.pendingIncomingMessages.removeAll()
self.enqueuedMessageIds.removeAll()
self.isProcessingIncomingMessages = false
// Cancel stale retry timers from previous connection
@@ -1566,7 +1567,18 @@ final class SessionManager {
DialogRepository.shared.updateDialogFromMessages(opponentKey: safeKey)
}
/// Dedup set for the current processing queue prevents the same messageId
/// from being enqueued twice (real-time + sync overlap). Cleared after queue drains.
private var enqueuedMessageIds: Set<String> = []
private func enqueueIncomingMessage(_ packet: PacketMessage) {
// Queue-level dedup: if the same messageId is already pending, skip.
// This catches the common real-time + sync overlap case without
// burning CPU on duplicate crypto + DB upsert.
let msgId = packet.messageId
if !msgId.isEmpty && enqueuedMessageIds.contains(msgId) { return }
if !msgId.isEmpty { enqueuedMessageIds.insert(msgId) }
pendingIncomingMessages.append(packet)
guard !isProcessingIncomingMessages else { return }
isProcessingIncomingMessages = true
@@ -1595,6 +1607,7 @@ final class SessionManager {
if batching { MessageRepository.shared.endBatchUpdates() }
isProcessingIncomingMessages = false
enqueuedMessageIds.removeAll()
signalQueueDrained()
}
@@ -1612,6 +1625,16 @@ final class SessionManager {
let opponentKey = context.dialogKey
let isGroupDialog = context.kind == .group
let wasKnownBefore = MessageRepository.shared.hasMessage(packet.messageId)
// Optimization: skip expensive crypto + upsert for incoming messages
// already stored in DB. Only outgoing messages need re-processing
// (sync may update delivery status from .waiting .delivered).
// This also eliminates any race-condition window between real-time
// and sync delivery of the same messageId.
if wasKnownBefore && !fromMe {
return
}
let groupKey: String? = {
guard isGroupDialog, let currentPrivateKeyHex else { return nil }
return GroupRepository.shared.groupKey(