mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 18:45:20 +00:00
refactor(nostr): add NostrInboxService and route geohash/DM subscriptions through it; move de-dup away from ChatViewModel
- Introduce Services/NostrInboxService to centralize subscribe/unsubscribe + event de-dup.\n- Wire ChatViewModel resubscribeCurrentGeohash, switchLocationChannel, sampling, and global DM subscribe to use the service.\n- Remove ChatViewModel's local processedNostrEvents tracking.
This commit is contained in:
@@ -58,6 +58,8 @@
|
|||||||
049BD3B52E51F319001A566B /* MessageRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3B02E51F319001A566B /* MessageRouter.swift */; };
|
049BD3B52E51F319001A566B /* MessageRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3B02E51F319001A566B /* MessageRouter.swift */; };
|
||||||
ABCD00022E5CCCC300162C4A /* ReadReceiptTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */; };
|
ABCD00022E5CCCC300162C4A /* ReadReceiptTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */; };
|
||||||
ABCD00032E5CCCC300162C4A /* ReadReceiptTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */; };
|
ABCD00032E5CCCC300162C4A /* ReadReceiptTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */; };
|
||||||
|
ABCD00052E5CCCC300162C4C /* NostrInboxService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD00042E5CCCC300162C4C /* NostrInboxService.swift */; };
|
||||||
|
ABCD00062E5CCCC300162C4D /* NostrInboxService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCD00042E5CCCC300162C4C /* NostrInboxService.swift */; };
|
||||||
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; };
|
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; };
|
||||||
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
|
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
|
||||||
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
|
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
|
||||||
@@ -216,6 +218,7 @@
|
|||||||
049BD39F2E51DBF4001A566B /* PeerID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerID.swift; sourceTree = "<group>"; };
|
049BD39F2E51DBF4001A566B /* PeerID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerID.swift; sourceTree = "<group>"; };
|
||||||
049BD3A42E51DC0E001A566B /* MessageDeduplicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDeduplicator.swift; sourceTree = "<group>"; };
|
049BD3A42E51DC0E001A566B /* MessageDeduplicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDeduplicator.swift; sourceTree = "<group>"; };
|
||||||
ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptTracker.swift; sourceTree = "<group>"; };
|
ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptTracker.swift; sourceTree = "<group>"; };
|
||||||
|
ABCD00042E5CCCC300162C4C /* NostrInboxService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrInboxService.swift; sourceTree = "<group>"; };
|
||||||
049BD3AA2E51E38E001A566B /* PeerIDResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerIDResolver.swift; sourceTree = "<group>"; };
|
049BD3AA2E51E38E001A566B /* PeerIDResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerIDResolver.swift; sourceTree = "<group>"; };
|
||||||
049BD3AD2E51ED60001A566B /* Transport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transport.swift; sourceTree = "<group>"; };
|
049BD3AD2E51ED60001A566B /* Transport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transport.swift; sourceTree = "<group>"; };
|
||||||
049BD3B02E51F319001A566B /* MessageRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRouter.swift; sourceTree = "<group>"; };
|
049BD3B02E51F319001A566B /* MessageRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRouter.swift; sourceTree = "<group>"; };
|
||||||
@@ -524,6 +527,7 @@
|
|||||||
children = (
|
children = (
|
||||||
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */,
|
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */,
|
||||||
ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */,
|
ABCD00012E5CCCC300162C4A /* ReadReceiptTracker.swift */,
|
||||||
|
ABCD00042E5CCCC300162C4C /* NostrInboxService.swift */,
|
||||||
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */,
|
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */,
|
||||||
047502B82E560F690083520F /* RelayController.swift */,
|
047502B82E560F690083520F /* RelayController.swift */,
|
||||||
0475028B2E54171C0083520F /* LocationChannelManager.swift */,
|
0475028B2E54171C0083520F /* LocationChannelManager.swift */,
|
||||||
@@ -748,6 +752,7 @@
|
|||||||
files = (
|
files = (
|
||||||
048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */,
|
048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */,
|
||||||
ABCD00022E5CCCC300162C4A /* ReadReceiptTracker.swift in Sources */,
|
ABCD00022E5CCCC300162C4A /* ReadReceiptTracker.swift in Sources */,
|
||||||
|
ABCD00052E5CCCC300162C4C /* NostrInboxService.swift in Sources */,
|
||||||
1234567890ABCDEFFEDCBA13 /* PeerDisplayNameResolver.swift in Sources */,
|
1234567890ABCDEFFEDCBA13 /* PeerDisplayNameResolver.swift in Sources */,
|
||||||
AA77BB12CC22DD33EE44FF56 /* VerificationService.swift in Sources */,
|
AA77BB12CC22DD33EE44FF56 /* VerificationService.swift in Sources */,
|
||||||
AA77BB15CC22DD33EE44FF59 /* VerificationViews.swift in Sources */,
|
AA77BB15CC22DD33EE44FF59 /* VerificationViews.swift in Sources */,
|
||||||
@@ -808,6 +813,7 @@
|
|||||||
files = (
|
files = (
|
||||||
048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */,
|
048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */,
|
||||||
ABCD00032E5CCCC300162C4A /* ReadReceiptTracker.swift in Sources */,
|
ABCD00032E5CCCC300162C4A /* ReadReceiptTracker.swift in Sources */,
|
||||||
|
ABCD00062E5CCCC300162C4D /* NostrInboxService.swift in Sources */,
|
||||||
1234567890ABCDEFFEDCBA14 /* PeerDisplayNameResolver.swift in Sources */,
|
1234567890ABCDEFFEDCBA14 /* PeerDisplayNameResolver.swift in Sources */,
|
||||||
AA77BB11CC22DD33EE44FF55 /* VerificationService.swift in Sources */,
|
AA77BB11CC22DD33EE44FF55 /* VerificationService.swift in Sources */,
|
||||||
AA77BB14CC22DD33EE44FF58 /* VerificationViews.swift in Sources */,
|
AA77BB14CC22DD33EE44FF58 /* VerificationViews.swift in Sources */,
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
//
|
||||||
|
// NostrInboxService.swift
|
||||||
|
// bitchat
|
||||||
|
//
|
||||||
|
// Centralizes Nostr subscribe/unsubscribe and event de-duplication for inbox flows.
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
// For more information, see <https://unlicense.org>
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class NostrInboxService {
|
||||||
|
private var processedIDs: Set<String> = []
|
||||||
|
private var order: [String] = []
|
||||||
|
private let cap: Int
|
||||||
|
|
||||||
|
init(capacity: Int = TransportConfig.uiProcessedNostrEventsCap) {
|
||||||
|
self.cap = capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
private func track(_ id: String) -> Bool {
|
||||||
|
if processedIDs.contains(id) { return false }
|
||||||
|
processedIDs.insert(id)
|
||||||
|
order.append(id)
|
||||||
|
if order.count > cap {
|
||||||
|
let overflow = order.count - cap
|
||||||
|
for _ in 0..<overflow {
|
||||||
|
if let oldest = order.first {
|
||||||
|
order.removeFirst()
|
||||||
|
processedIDs.remove(oldest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func reset() {
|
||||||
|
processedIDs.removeAll()
|
||||||
|
order.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to public geohash ephemeral events (kind 20000)
|
||||||
|
@discardableResult
|
||||||
|
func subscribeGeohashEphemeral(
|
||||||
|
geohash: String,
|
||||||
|
lookbackSeconds: TimeInterval,
|
||||||
|
limit: Int,
|
||||||
|
relayCount: Int,
|
||||||
|
subID: String? = nil,
|
||||||
|
handler: @escaping (NostrProtocol.Event) -> Void
|
||||||
|
) -> String {
|
||||||
|
let id = subID ?? "geo-\(geohash)"
|
||||||
|
let filter = NostrFilter.geohashEphemeral(
|
||||||
|
geohash,
|
||||||
|
since: Date().addingTimeInterval(-lookbackSeconds),
|
||||||
|
limit: limit
|
||||||
|
)
|
||||||
|
let relays = GeoRelayDirectory.shared.closestRelays(toGeohash: geohash, count: relayCount)
|
||||||
|
NostrRelayManager.shared.subscribe(filter: filter, id: id, relayUrls: relays) { [weak self] event in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
||||||
|
guard self.track(event.id) else { return }
|
||||||
|
handler(event)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to gift wraps (DMs) for a given pubkey
|
||||||
|
@discardableResult
|
||||||
|
func subscribeGiftWrapsFor(
|
||||||
|
pubkeyHex: String,
|
||||||
|
lookbackSeconds: TimeInterval,
|
||||||
|
subID: String? = nil,
|
||||||
|
handler: @escaping (NostrProtocol.Event) -> Void
|
||||||
|
) -> String {
|
||||||
|
let id = subID ?? "dm-\(pubkeyHex.prefix(8))"
|
||||||
|
let filter = NostrFilter.giftWrapsFor(pubkey: pubkeyHex, since: Date().addingTimeInterval(-lookbackSeconds))
|
||||||
|
NostrRelayManager.shared.subscribe(filter: filter, id: id) { [weak self] event in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard self.track(event.id) else { return }
|
||||||
|
handler(event)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribe(id: String) {
|
||||||
|
NostrRelayManager.shared.unsubscribe(id: id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -348,9 +348,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
var meshService: Transport = BLEService()
|
var meshService: Transport = BLEService()
|
||||||
private var nostrRelayManager: NostrRelayManager?
|
private var nostrRelayManager: NostrRelayManager?
|
||||||
// PeerManager replaced by UnifiedPeerService
|
// PeerManager replaced by UnifiedPeerService
|
||||||
private var processedNostrEvents = Set<String>() // Simple deduplication
|
// NostrInboxService handles event de-duplication
|
||||||
private var processedNostrEventOrder: [String] = []
|
|
||||||
private let maxProcessedNostrEvents = TransportConfig.uiProcessedNostrEventsCap
|
|
||||||
private let userDefaults = UserDefaults.standard
|
private let userDefaults = UserDefaults.standard
|
||||||
private let nicknameKey = "bitchat.nickname"
|
private let nicknameKey = "bitchat.nickname"
|
||||||
// Location channel state (macOS supports manual geohash selection)
|
// Location channel state (macOS supports manual geohash selection)
|
||||||
@@ -359,6 +357,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
private var geoDmSubscriptionID: String? = nil
|
private var geoDmSubscriptionID: String? = nil
|
||||||
private var currentGeohash: String? = nil
|
private var currentGeohash: String? = nil
|
||||||
private var geoNicknames: [String: String] = [:] // pubkeyHex(lowercased) -> nickname
|
private var geoNicknames: [String: String] = [:] // pubkeyHex(lowercased) -> nickname
|
||||||
|
private let nostrInbox = NostrInboxService()
|
||||||
|
|
||||||
// MARK: - Caches
|
// MARK: - Caches
|
||||||
|
|
||||||
@@ -901,20 +900,15 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
startGeoParticipantsTimer()
|
startGeoParticipantsTimer()
|
||||||
// Unsubscribe + resubscribe
|
// Unsubscribe + resubscribe
|
||||||
NostrRelayManager.shared.unsubscribe(id: subID)
|
NostrRelayManager.shared.unsubscribe(id: subID)
|
||||||
let filter = NostrFilter.geohashEphemeral(
|
nostrInbox.subscribeGeohashEphemeral(
|
||||||
ch.geohash,
|
geohash: ch.geohash,
|
||||||
since: Date().addingTimeInterval(-TransportConfig.nostrGeohashInitialLookbackSeconds),
|
lookbackSeconds: TransportConfig.nostrGeohashInitialLookbackSeconds,
|
||||||
limit: TransportConfig.nostrGeohashInitialLimit
|
limit: TransportConfig.nostrGeohashInitialLimit,
|
||||||
)
|
relayCount: TransportConfig.nostrGeoRelayCount,
|
||||||
let subRelays = GeoRelayDirectory.shared.closestRelays(
|
subID: subID
|
||||||
toGeohash: ch.geohash,
|
) { [weak self] event in
|
||||||
count: TransportConfig.nostrGeoRelayCount
|
|
||||||
)
|
|
||||||
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
|
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
||||||
if self.processedNostrEvents.contains(event.id) { return }
|
|
||||||
self.processedNostrEvents.insert(event.id)
|
|
||||||
if let gh = self.currentGeohash,
|
if let gh = self.currentGeohash,
|
||||||
let myGeoIdentity = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh),
|
let myGeoIdentity = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh),
|
||||||
myGeoIdentity.publicKeyHex.lowercased() == event.pubkey.lowercased() {
|
myGeoIdentity.publicKeyHex.lowercased() == event.pubkey.lowercased() {
|
||||||
@@ -971,11 +965,12 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let id = try NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash)
|
let id = try NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash)
|
||||||
let dmSub = "geo-dm-\(ch.geohash)"
|
let dmSub = "geo-dm-\(ch.geohash)"
|
||||||
geoDmSubscriptionID = dmSub
|
geoDmSubscriptionID = dmSub
|
||||||
let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds))
|
nostrInbox.subscribeGiftWrapsFor(
|
||||||
NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in
|
pubkeyHex: id.publicKeyHex,
|
||||||
|
lookbackSeconds: TransportConfig.nostrDMSubscribeLookbackSeconds,
|
||||||
|
subID: dmSub
|
||||||
|
) { [weak self] giftWrap in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if self.processedNostrEvents.contains(giftWrap.id) { return }
|
|
||||||
self.recordProcessedEvent(giftWrap.id)
|
|
||||||
guard let (content, senderPubkey, rumorTs) = try? NostrProtocol.decryptPrivateMessage(giftWrap: giftWrap, recipientIdentity: id) else { return }
|
guard let (content, senderPubkey, rumorTs) = try? NostrProtocol.decryptPrivateMessage(giftWrap: giftWrap, recipientIdentity: id) else { return }
|
||||||
guard content.hasPrefix("bitchat1:") else { return }
|
guard content.hasPrefix("bitchat1:") else { return }
|
||||||
guard let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))),
|
guard let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))),
|
||||||
@@ -1512,8 +1507,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
publicBuffer.removeAll(keepingCapacity: false)
|
publicBuffer.removeAll(keepingCapacity: false)
|
||||||
activeChannel = channel
|
activeChannel = channel
|
||||||
// Reset deduplication set and optionally hydrate timeline for mesh
|
// Reset deduplication set and optionally hydrate timeline for mesh
|
||||||
processedNostrEvents.removeAll()
|
nostrInbox.reset()
|
||||||
processedNostrEventOrder.removeAll()
|
|
||||||
switch channel {
|
switch channel {
|
||||||
case .mesh:
|
case .mesh:
|
||||||
messages = meshTimeline
|
messages = meshTimeline
|
||||||
@@ -1557,19 +1551,16 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let subID = "geo-\(ch.geohash)"
|
let subID = "geo-\(ch.geohash)"
|
||||||
geoSubscriptionID = subID
|
geoSubscriptionID = subID
|
||||||
startGeoParticipantsTimer()
|
startGeoParticipantsTimer()
|
||||||
let filter = NostrFilter.geohashEphemeral(
|
nostrInbox.subscribeGeohashEphemeral(
|
||||||
ch.geohash,
|
geohash: ch.geohash,
|
||||||
since: Date().addingTimeInterval(-TransportConfig.nostrGeohashInitialLookbackSeconds),
|
lookbackSeconds: TransportConfig.nostrGeohashInitialLookbackSeconds,
|
||||||
limit: TransportConfig.nostrGeohashInitialLimit
|
limit: TransportConfig.nostrGeohashInitialLimit,
|
||||||
)
|
relayCount: TransportConfig.nostrGeoRelayCount,
|
||||||
let subRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5)
|
subID: subID
|
||||||
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
|
) { [weak self] event in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
// Only handle ephemeral kind 20000 with matching tag
|
// Only handle ephemeral kind 20000 with matching tag
|
||||||
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
||||||
// Deduplicate
|
|
||||||
if self.processedNostrEvents.contains(event.id) { return }
|
|
||||||
self.recordProcessedEvent(event.id)
|
|
||||||
// Log incoming tags for diagnostics
|
// Log incoming tags for diagnostics
|
||||||
let tagSummary = event.tags.map { "[" + $0.joined(separator: ",") + "]" }.joined(separator: ",")
|
let tagSummary = event.tags.map { "[" + $0.joined(separator: ",") + "]" }.joined(separator: ",")
|
||||||
SecureLogger.log("GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tags=\(tagSummary)",
|
SecureLogger.log("GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tags=\(tagSummary)",
|
||||||
@@ -1646,12 +1637,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// pared back logging: subscribe debug only
|
// pared back logging: subscribe debug only
|
||||||
SecureLogger.log("GeoDM: subscribing DMs pub=\(id.publicKeyHex.prefix(8))… sub=\(dmSub)",
|
SecureLogger.log("GeoDM: subscribing DMs pub=\(id.publicKeyHex.prefix(8))… sub=\(dmSub)",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds))
|
nostrInbox.subscribeGiftWrapsFor(pubkeyHex: id.publicKeyHex, lookbackSeconds: TransportConfig.nostrDMSubscribeLookbackSeconds, subID: dmSub) { [weak self] giftWrap in
|
||||||
NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in
|
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
// Dedup basic
|
|
||||||
if self.processedNostrEvents.contains(giftWrap.id) { return }
|
|
||||||
self.recordProcessedEvent(giftWrap.id)
|
|
||||||
// Decrypt with per-geohash identity
|
// Decrypt with per-geohash identity
|
||||||
guard let (content, senderPubkey, rumorTs) = try? NostrProtocol.decryptPrivateMessage(giftWrap: giftWrap, recipientIdentity: id) else {
|
guard let (content, senderPubkey, rumorTs) = try? NostrProtocol.decryptPrivateMessage(giftWrap: giftWrap, recipientIdentity: id) else {
|
||||||
SecureLogger.log("GeoDM: failed decrypt giftWrap id=\(giftWrap.id.prefix(8))…",
|
SecureLogger.log("GeoDM: failed decrypt giftWrap id=\(giftWrap.id.prefix(8))…",
|
||||||
@@ -1918,7 +1905,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
//
|
//
|
||||||
for (subID, gh) in geoSamplingSubs where toRemove.contains(gh) {
|
for (subID, gh) in geoSamplingSubs where toRemove.contains(gh) {
|
||||||
NostrRelayManager.shared.unsubscribe(id: subID)
|
nostrInbox.unsubscribe(id: subID)
|
||||||
geoSamplingSubs.removeValue(forKey: subID)
|
geoSamplingSubs.removeValue(forKey: subID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1926,13 +1913,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
for gh in toAdd {
|
for gh in toAdd {
|
||||||
let subID = "geo-sample-\(gh)"
|
let subID = "geo-sample-\(gh)"
|
||||||
geoSamplingSubs[subID] = gh
|
geoSamplingSubs[subID] = gh
|
||||||
let filter = NostrFilter.geohashEphemeral(
|
nostrInbox.subscribeGeohashEphemeral(
|
||||||
gh,
|
geohash: gh,
|
||||||
since: Date().addingTimeInterval(-TransportConfig.nostrGeohashSampleLookbackSeconds),
|
lookbackSeconds: TransportConfig.nostrGeohashSampleLookbackSeconds,
|
||||||
limit: TransportConfig.nostrGeohashSampleLimit
|
limit: TransportConfig.nostrGeohashSampleLimit,
|
||||||
)
|
relayCount: TransportConfig.nostrGeoRelayCount,
|
||||||
let subRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: gh, count: 5)
|
subID: subID
|
||||||
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
|
) { [weak self] event in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
||||||
// Update participants for this specific geohash
|
// Update participants for this specific geohash
|
||||||
@@ -2008,19 +1995,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dedup helper with small memory cap
|
// Dedup helper with small memory cap
|
||||||
private func recordProcessedEvent(_ id: String) {
|
private func recordProcessedEvent(_ id: String) {}
|
||||||
processedNostrEvents.insert(id)
|
|
||||||
processedNostrEventOrder.append(id)
|
|
||||||
if processedNostrEventOrder.count > maxProcessedNostrEvents {
|
|
||||||
let overflow = processedNostrEventOrder.count - maxProcessedNostrEvents
|
|
||||||
for _ in 0..<overflow {
|
|
||||||
if let old = processedNostrEventOrder.first {
|
|
||||||
processedNostrEventOrder.removeFirst()
|
|
||||||
processedNostrEvents.remove(old)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends an encrypted private message to a specific peer.
|
/// Sends an encrypted private message to a specific peer.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -4939,12 +4914,11 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
|
|
||||||
// Subscribe to Nostr messages
|
// Subscribe to Nostr messages
|
||||||
let filter = NostrFilter.giftWrapsFor(
|
let _ = nostrInbox.subscribeGiftWrapsFor(
|
||||||
pubkey: currentIdentity.publicKeyHex,
|
pubkeyHex: currentIdentity.publicKeyHex,
|
||||||
since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds) // Last 24 hours
|
lookbackSeconds: TransportConfig.nostrDMSubscribeLookbackSeconds,
|
||||||
)
|
subID: "chat-messages"
|
||||||
|
) { [weak self] event in
|
||||||
nostrRelayManager?.subscribe(filter: filter, id: "chat-messages") { [weak self] event in
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self?.handleNostrMessage(event)
|
self?.handleNostrMessage(event)
|
||||||
}
|
}
|
||||||
@@ -4953,9 +4927,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func handleNostrMessage(_ giftWrap: NostrEvent) {
|
private func handleNostrMessage(_ giftWrap: NostrEvent) {
|
||||||
// Simple deduplication
|
// De-duplication handled by NostrInboxService
|
||||||
if processedNostrEvents.contains(giftWrap.id) { return }
|
|
||||||
processedNostrEvents.insert(giftWrap.id)
|
|
||||||
|
|
||||||
// Client-side filtering: ignore messages older than 24 hours
|
// Client-side filtering: ignore messages older than 24 hours
|
||||||
// Add 15 minutes buffer since gift wrap timestamps are randomized ±15 minutes
|
// Add 15 minutes buffer since gift wrap timestamps are randomized ±15 minutes
|
||||||
|
|||||||
Reference in New Issue
Block a user