mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 18:05:21 +00:00
Prevent mesh self-sync duplicates (#856)
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -642,6 +651,10 @@ final class BLEService: NSObject {
|
|||||||
// Delegate to the full API with default routing
|
// Delegate to the full API with default routing
|
||||||
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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|||||||
Reference in New Issue
Block a user