Prevent mesh self-sync duplicates (#856)

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-10-23 10:41:28 +02:00
committed by GitHub
co-authored by jack
parent 5034732515
commit 83779240ae
6 changed files with 68 additions and 15 deletions
+2 -2
View File
@@ -178,7 +178,7 @@ protocol BitchatDelegate: AnyObject {
// Bluetooth state updates for user notifications // Bluetooth state updates for user notifications
func didUpdateBluetoothState(_ state: CBManagerState) func didUpdateBluetoothState(_ state: CBManagerState)
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?)
} }
// Provide default implementation to make it effectively optional // Provide default implementation to make it effectively optional
@@ -195,7 +195,7 @@ extension BitchatDelegate {
// Default empty implementation // Default empty implementation
} }
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) { func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?) {
// Default empty implementation // Default empty implementation
} }
} }
+32 -2
View File
@@ -67,6 +67,7 @@ final class BLEService: NSObject {
// 4. Efficient Message Deduplication // 4. Efficient Message Deduplication
private let messageDeduplicator = MessageDeduplicator() private let messageDeduplicator = MessageDeduplicator()
private var selfBroadcastMessageIDs: [String: (id: String, timestamp: Date)] = [:]
private lazy var mediaDateFormatter: DateFormatter = { private lazy var mediaDateFormatter: DateFormatter = {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd_HHmmss" formatter.dateFormat = "yyyyMMdd_HHmmss"
@@ -358,6 +359,9 @@ final class BLEService: NSObject {
setNickname(currentNickname) setNickname(currentNickname)
messageDeduplicator.reset() messageDeduplicator.reset()
messageQueue.async(flags: .barrier) { [weak self] in
self?.selfBroadcastMessageIDs.removeAll()
}
requestPeerDataPublish() requestPeerDataPublish()
startServices() startServices()
} }
@@ -384,11 +388,13 @@ final class BLEService: NSObject {
// Public broadcast // Public broadcast
// Create packet with explicit fields so we can sign it // Create packet with explicit fields so we can sign it
let sendDate = timestamp ?? Date()
let sendTimestampMs = UInt64(sendDate.timeIntervalSince1970 * 1000)
let basePacket = BitchatPacket( let basePacket = BitchatPacket(
type: MessageType.message.rawValue, type: MessageType.message.rawValue,
senderID: Data(hexString: myPeerID.id) ?? Data(), senderID: Data(hexString: myPeerID.id) ?? Data(),
recipientID: nil, recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000), timestamp: sendTimestampMs,
payload: Data(content.utf8), payload: Data(content.utf8),
signature: nil, signature: nil,
ttl: messageTTL ttl: messageTTL
@@ -401,6 +407,9 @@ final class BLEService: NSObject {
let senderHex = signedPacket.senderID.hexEncodedString() let senderHex = signedPacket.senderID.hexEncodedString()
let dedupID = "\(senderHex)-\(signedPacket.timestamp)-\(signedPacket.type)" let dedupID = "\(senderHex)-\(signedPacket.timestamp)-\(signedPacket.type)"
messageDeduplicator.markProcessed(dedupID) messageDeduplicator.markProcessed(dedupID)
if let messageID {
selfBroadcastMessageIDs[dedupID] = (id: messageID, timestamp: sendDate)
}
// Call synchronously since we're already on background queue // Call synchronously since we're already on background queue
broadcastPacket(signedPacket) broadcastPacket(signedPacket)
// Track our own broadcast for sync // Track our own broadcast for sync
@@ -643,6 +652,10 @@ final class BLEService: NSObject {
sendMessage(content, mentions: mentions, to: nil, messageID: nil, timestamp: nil) sendMessage(content, mentions: mentions, to: nil, messageID: nil, timestamp: nil)
} }
func sendMessage(_ content: String, mentions: [String], messageID: String, timestamp: Date) {
sendMessage(content, mentions: mentions, to: nil, messageID: messageID, timestamp: timestamp)
}
func sendPrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) { func sendPrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) {
sendPrivateMessage(content, to: peerID, messageID: messageID) sendPrivateMessage(content, to: peerID, messageID: messageID)
} }
@@ -3417,8 +3430,18 @@ extension BLEService {
SecureLogger.debug("💬 [\(senderNickname)] TTL:\(packet.ttl) (\(pathTag)): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: .session) SecureLogger.debug("💬 [\(senderNickname)] TTL:\(packet.ttl) (\(pathTag)): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: .session)
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000) let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
var resolvedSelfMessageID: String? = nil
if peerID == myPeerID {
let senderHex = packet.senderID.hexEncodedString()
let dedupID = "\(senderHex)-\(packet.timestamp)-\(packet.type)"
resolvedSelfMessageID = selfBroadcastMessageIDs.removeValue(forKey: dedupID)?.id
}
notifyUI { [weak self] in notifyUI { [weak self] in
self?.delegate?.didReceivePublicMessage(from: peerID, nickname: senderNickname, content: content, timestamp: ts) self?.delegate?.didReceivePublicMessage(from: peerID,
nickname: senderNickname,
content: content,
timestamp: ts,
messageID: resolvedSelfMessageID)
} }
} }
@@ -3772,6 +3795,13 @@ extension BLEService {
self.pendingDirectedRelays = cleaned self.pendingDirectedRelays = cleaned
} }
} }
messageQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
guard !self.selfBroadcastMessageIDs.isEmpty else { return }
let cutoff = now.addingTimeInterval(-TransportConfig.messageDedupMaxAgeSeconds)
self.selfBroadcastMessageIDs = self.selfBroadcastMessageIDs.filter { cutoff <= $0.value.timestamp }
}
} }
private func updateScanningDutyCycle(connectedCount: Int) { private func updateScanningDutyCycle(connectedCount: Int) {
+5
View File
@@ -45,6 +45,7 @@ protocol Transport: AnyObject {
// Messaging // Messaging
func sendMessage(_ content: String, mentions: [String]) func sendMessage(_ content: String, mentions: [String])
func sendMessage(_ content: String, mentions: [String], messageID: String, timestamp: Date)
func sendPrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) func sendPrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String)
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) func sendReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID)
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool)
@@ -65,6 +66,10 @@ extension Transport {
func sendFileBroadcast(_ packet: BitchatFilePacket, transferId: String) {} func sendFileBroadcast(_ packet: BitchatFilePacket, transferId: String) {}
func sendFilePrivate(_ packet: BitchatFilePacket, to peerID: PeerID, transferId: String) {} func sendFilePrivate(_ packet: BitchatFilePacket, to peerID: PeerID, transferId: String) {}
func cancelTransfer(_ transferId: String) {} func cancelTransfer(_ transferId: String) {}
func sendMessage(_ content: String, mentions: [String], messageID: String, timestamp: Date) {
sendMessage(content, mentions: mentions)
}
} }
protocol TransportPeerEventsDelegate: AnyObject { protocol TransportPeerEventsDelegate: AnyObject {
+27 -9
View File
@@ -1529,15 +1529,25 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// UI updates automatically via @Published var messages // UI updates automatically via @Published var messages
updateChannelActivityTimeThenSend(content: content, trimmed: trimmed, mentions: mentions, geoContext: geoContext) updateChannelActivityTimeThenSend(content: content,
trimmed: trimmed,
mentions: mentions,
geoContext: geoContext,
messageID: message.id,
timestamp: message.timestamp)
} }
private func updateChannelActivityTimeThenSend(content: String, trimmed: String, mentions: [String], geoContext: GeoOutgoingContext?) { private func updateChannelActivityTimeThenSend(content: String,
trimmed: String,
mentions: [String],
geoContext: GeoOutgoingContext?,
messageID: String,
timestamp: Date) {
switch activeChannel { switch activeChannel {
case .mesh: case .mesh:
lastPublicActivityAt["mesh"] = Date() lastPublicActivityAt["mesh"] = Date()
// Send via mesh with mentions // Send via mesh with mentions
meshService.sendMessage(content, mentions: mentions) meshService.sendMessage(content, mentions: mentions, messageID: messageID, timestamp: timestamp)
case .location(let ch): case .location(let ch):
lastPublicActivityAt["geo:\(ch.geohash)"] = Date() lastPublicActivityAt["geo:\(ch.geohash)"] = Date()
guard let context = geoContext, context.channel.geohash == ch.geohash else { guard let context = geoContext, context.channel.geohash == ch.geohash else {
@@ -3335,7 +3345,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// In public chat - send to active public channel // In public chat - send to active public channel
switch activeChannel { switch activeChannel {
case .mesh: case .mesh:
meshService.sendMessage(screenshotMessage, mentions: []) meshService.sendMessage(screenshotMessage,
mentions: [],
messageID: UUID().uuidString,
timestamp: Date())
case .location(let ch): case .location(let ch):
Task { @MainActor in Task { @MainActor in
do { do {
@@ -5050,12 +5063,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
} }
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) { func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?) {
Task { @MainActor in Task { @MainActor in
let normalized = content.trimmingCharacters(in: .whitespacesAndNewlines) let normalized = content.trimmingCharacters(in: .whitespacesAndNewlines)
let publicMentions = parseMentions(from: normalized) let publicMentions = parseMentions(from: normalized)
let msg = BitchatMessage( let msg = BitchatMessage(
id: UUID().uuidString, id: messageID,
sender: nickname, sender: nickname,
content: normalized, content: normalized,
timestamp: timestamp, timestamp: timestamp,
@@ -5522,7 +5535,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
return return
} }
// Default: send over mesh // Default: send over mesh
meshService.sendMessage(content, mentions: []) meshService.sendMessage(content,
mentions: [],
messageID: UUID().uuidString,
timestamp: Date())
} }
// MARK: - Simplified Nostr Integration (Inlined from MessageRouter) // MARK: - Simplified Nostr Integration (Inlined from MessageRouter)
@@ -6308,8 +6324,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Persist mesh messages to mesh timeline always // Persist mesh messages to mesh timeline always
if !isGeo && finalMessage.sender != "system" { if !isGeo && finalMessage.sender != "system" {
meshTimeline.append(finalMessage) if !meshTimeline.contains(where: { $0.id == finalMessage.id }) {
trimMeshTimelineIfNeeded() meshTimeline.append(finalMessage)
trimMeshTimelineIfNeeded()
}
} }
// Persist geochat messages to per-geohash timeline // Persist geochat messages to per-geohash timeline
+1 -1
View File
@@ -298,5 +298,5 @@ private final class MockBitchatDelegate: BitchatDelegate {
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {} func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {} func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {}
func didUpdateBluetoothState(_ state: CBManagerState) {} func didUpdateBluetoothState(_ state: CBManagerState) {}
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {} func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?) {}
} }
@@ -209,7 +209,7 @@ extension FragmentationTests {
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {} func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {} func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {}
func didUpdateBluetoothState(_ state: CBManagerState) {} func didUpdateBluetoothState(_ state: CBManagerState) {}
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) { func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?) {
publicMessages.append((peerID, nickname, content)) publicMessages.append((peerID, nickname, content))
} }
func didReceiveRegionalPublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) {} func didReceiveRegionalPublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) {}