mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
PeerID 14/n: Transport and its dependents (#753)
* Rearrange `Transport`’s properties and functions * `NostrTransport`: group Transport-related code together * `BLEService`: group Transport-related code together * Extract `NotificationStreamAssembler` into a file * Move private functions to a dedicated extension * PeerID 14/n: `Transport` and its dependents
This commit is contained in:
+1939
-1996
File diff suppressed because it is too large
Load Diff
@@ -148,9 +148,9 @@ final class CommandProcessor {
|
|||||||
|
|
||||||
if chatViewModel?.selectedPrivateChatPeer != nil {
|
if chatViewModel?.selectedPrivateChatPeer != nil {
|
||||||
// In private chat
|
// In private chat
|
||||||
if let peerNickname = meshService?.peerNickname(peerID: targetPeerID) {
|
if let peerNickname = meshService?.peerNickname(peerID: PeerID(str: targetPeerID)) {
|
||||||
let personalMessage = "* \(emoji) \(myNickname) \(action) you\(suffix) *"
|
let personalMessage = "* \(emoji) \(myNickname) \(action) you\(suffix) *"
|
||||||
meshService?.sendPrivateMessage(personalMessage, to: targetPeerID,
|
meshService?.sendPrivateMessage(personalMessage, to: PeerID(str: targetPeerID),
|
||||||
recipientNickname: peerNickname,
|
recipientNickname: peerNickname,
|
||||||
messageID: UUID().uuidString)
|
messageID: UUID().uuidString)
|
||||||
// Also add a local system message so the sender sees a natural-language confirmation
|
// Also add a local system message so the sender sees a natural-language confirmation
|
||||||
@@ -214,7 +214,7 @@ final class CommandProcessor {
|
|||||||
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
||||||
|
|
||||||
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
||||||
let fingerprint = meshService?.getFingerprint(for: peerID) {
|
let fingerprint = meshService?.getFingerprint(for: PeerID(str: peerID)) {
|
||||||
if identityManager.isBlocked(fingerprint: fingerprint) {
|
if identityManager.isBlocked(fingerprint: fingerprint) {
|
||||||
return .success(message: "\(nickname) is already blocked")
|
return .success(message: "\(nickname) is already blocked")
|
||||||
}
|
}
|
||||||
@@ -258,7 +258,7 @@ final class CommandProcessor {
|
|||||||
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
|
||||||
|
|
||||||
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
||||||
let fingerprint = meshService?.getFingerprint(for: peerID) {
|
let fingerprint = meshService?.getFingerprint(for: PeerID(str: peerID)) {
|
||||||
if !identityManager.isBlocked(fingerprint: fingerprint) {
|
if !identityManager.isBlocked(fingerprint: fingerprint) {
|
||||||
return .success(message: "\(nickname) is not blocked")
|
return .success(message: "\(nickname) is not blocked")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ final class MessageRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendPrivate(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) {
|
func sendPrivate(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) {
|
||||||
let reachableMesh = mesh.isPeerReachable(peerID.id)
|
let reachableMesh = mesh.isPeerReachable(peerID)
|
||||||
if reachableMesh {
|
if reachableMesh {
|
||||||
SecureLogger.debug("Routing PM via mesh (reachable) to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
SecureLogger.debug("Routing PM via mesh (reachable) to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
// BLEService will initiate a handshake if needed and queue the message
|
// BLEService will initiate a handshake if needed and queue the message
|
||||||
mesh.sendPrivateMessage(content, to: peerID.id, recipientNickname: recipientNickname, messageID: messageID)
|
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
|
||||||
} else if canSendViaNostr(peerID: peerID) {
|
} else if canSendViaNostr(peerID: peerID) {
|
||||||
SecureLogger.debug("Routing PM via Nostr to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
SecureLogger.debug("Routing PM via Nostr to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
nostr.sendPrivateMessage(content, to: peerID.id, recipientNickname: recipientNickname, messageID: messageID)
|
nostr.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
|
||||||
} else {
|
} else {
|
||||||
// Queue for later (when mesh connects or Nostr mapping appears)
|
// Queue for later (when mesh connects or Nostr mapping appears)
|
||||||
if outbox[peerID] == nil { outbox[peerID] = [] }
|
if outbox[peerID] == nil { outbox[peerID] = [] }
|
||||||
@@ -56,30 +56,30 @@ final class MessageRouter {
|
|||||||
|
|
||||||
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) {
|
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) {
|
||||||
// Prefer mesh for reachable peers; BLE will queue if handshake is needed
|
// Prefer mesh for reachable peers; BLE will queue if handshake is needed
|
||||||
if mesh.isPeerReachable(peerID.id) {
|
if mesh.isPeerReachable(peerID) {
|
||||||
SecureLogger.debug("Routing READ ack via mesh (reachable) to \(peerID.id.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session)
|
SecureLogger.debug("Routing READ ack via mesh (reachable) to \(peerID.id.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session)
|
||||||
mesh.sendReadReceipt(receipt, to: peerID.id)
|
mesh.sendReadReceipt(receipt, to: peerID)
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.debug("Routing READ ack via Nostr to \(peerID.id.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session)
|
SecureLogger.debug("Routing READ ack via Nostr to \(peerID.id.prefix(8))… id=\(receipt.originalMessageID.prefix(8))…", category: .session)
|
||||||
nostr.sendReadReceipt(receipt, to: peerID.id)
|
nostr.sendReadReceipt(receipt, to: peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendDeliveryAck(_ messageID: String, to peerID: PeerID) {
|
func sendDeliveryAck(_ messageID: String, to peerID: PeerID) {
|
||||||
if mesh.isPeerReachable(peerID.id) {
|
if mesh.isPeerReachable(peerID) {
|
||||||
SecureLogger.debug("Routing DELIVERED ack via mesh (reachable) to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
SecureLogger.debug("Routing DELIVERED ack via mesh (reachable) to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
mesh.sendDeliveryAck(for: messageID, to: peerID.id)
|
mesh.sendDeliveryAck(for: messageID, to: peerID)
|
||||||
} else {
|
} else {
|
||||||
nostr.sendDeliveryAck(for: messageID, to: peerID.id)
|
nostr.sendDeliveryAck(for: messageID, to: peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
|
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
|
||||||
// Route via mesh when connected; else use Nostr
|
// Route via mesh when connected; else use Nostr
|
||||||
if mesh.isPeerConnected(peerID.id) {
|
if mesh.isPeerConnected(peerID) {
|
||||||
mesh.sendFavoriteNotification(to: peerID.id, isFavorite: isFavorite)
|
mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||||
} else {
|
} else {
|
||||||
nostr.sendFavoriteNotification(to: peerID.id, isFavorite: isFavorite)
|
nostr.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,12 +108,12 @@ final class MessageRouter {
|
|||||||
var remaining: [(content: String, nickname: String, messageID: String)] = []
|
var remaining: [(content: String, nickname: String, messageID: String)] = []
|
||||||
// Prefer mesh if connected; else try Nostr if mapping exists
|
// Prefer mesh if connected; else try Nostr if mapping exists
|
||||||
for (content, nickname, messageID) in queued {
|
for (content, nickname, messageID) in queued {
|
||||||
if mesh.isPeerReachable(peerID.id) {
|
if mesh.isPeerReachable(peerID) {
|
||||||
SecureLogger.debug("Outbox -> mesh for \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
SecureLogger.debug("Outbox -> mesh for \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
mesh.sendPrivateMessage(content, to: peerID.id, recipientNickname: nickname, messageID: messageID)
|
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID)
|
||||||
} else if canSendViaNostr(peerID: peerID) {
|
} else if canSendViaNostr(peerID: peerID) {
|
||||||
SecureLogger.debug("Outbox -> Nostr for \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
SecureLogger.debug("Outbox -> Nostr for \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
nostr.sendPrivateMessage(content, to: peerID.id, recipientNickname: nickname, messageID: messageID)
|
nostr.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID)
|
||||||
} else {
|
} else {
|
||||||
// Keep unsent items queued
|
// Keep unsent items queued
|
||||||
remaining.append((content, nickname, messageID))
|
remaining.append((content, nickname, messageID))
|
||||||
|
|||||||
@@ -4,46 +4,49 @@ import Combine
|
|||||||
|
|
||||||
// Minimal Nostr transport conforming to Transport for offline sending
|
// Minimal Nostr transport conforming to Transport for offline sending
|
||||||
final class NostrTransport: Transport {
|
final class NostrTransport: Transport {
|
||||||
weak var delegate: BitchatDelegate?
|
|
||||||
weak var peerEventsDelegate: TransportPeerEventsDelegate?
|
|
||||||
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
|
|
||||||
Just([]).eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
func currentPeerSnapshots() -> [TransportPeerSnapshot] { [] }
|
|
||||||
|
|
||||||
// Provide BLE short peer ID for BitChat embedding
|
// Provide BLE short peer ID for BitChat embedding
|
||||||
var senderPeerID: String = ""
|
var senderPeerID = PeerID(str: "")
|
||||||
|
|
||||||
// Throttle READ receipts to avoid relay rate limits
|
// Throttle READ receipts to avoid relay rate limits
|
||||||
private struct QueuedRead {
|
private struct QueuedRead {
|
||||||
let receipt: ReadReceipt
|
let receipt: ReadReceipt
|
||||||
let peerID: String
|
let peerID: PeerID
|
||||||
}
|
}
|
||||||
private var readQueue: [QueuedRead] = []
|
private var readQueue: [QueuedRead] = []
|
||||||
private var isSendingReadAcks = false
|
private var isSendingReadAcks = false
|
||||||
private let readAckInterval: TimeInterval = TransportConfig.nostrReadAckInterval
|
private let readAckInterval: TimeInterval = TransportConfig.nostrReadAckInterval
|
||||||
private let keychain: KeychainManagerProtocol
|
private let keychain: KeychainManagerProtocol
|
||||||
|
|
||||||
var myPeerID: String { senderPeerID }
|
|
||||||
var myNickname: String { "" }
|
|
||||||
func setNickname(_ nickname: String) { /* not used for Nostr */ }
|
|
||||||
|
|
||||||
init(keychain: KeychainManagerProtocol) {
|
init(keychain: KeychainManagerProtocol) {
|
||||||
self.keychain = keychain
|
self.keychain = keychain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Transport Protocol Conformance
|
||||||
|
|
||||||
|
weak var delegate: BitchatDelegate?
|
||||||
|
weak var peerEventsDelegate: TransportPeerEventsDelegate?
|
||||||
|
|
||||||
|
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
|
||||||
|
Just([]).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
func currentPeerSnapshots() -> [TransportPeerSnapshot] { [] }
|
||||||
|
|
||||||
|
var myPeerID: PeerID { senderPeerID }
|
||||||
|
var myNickname: String { "" }
|
||||||
|
func setNickname(_ nickname: String) { /* not used for Nostr */ }
|
||||||
|
|
||||||
func startServices() { /* no-op */ }
|
func startServices() { /* no-op */ }
|
||||||
func stopServices() { /* no-op */ }
|
func stopServices() { /* no-op */ }
|
||||||
func emergencyDisconnectAll() { /* no-op */ }
|
func emergencyDisconnectAll() { /* no-op */ }
|
||||||
|
|
||||||
func isPeerConnected(_ peerID: String) -> Bool { false }
|
func isPeerConnected(_ peerID: PeerID) -> Bool { false }
|
||||||
func isPeerReachable(_ peerID: String) -> Bool { false }
|
func isPeerReachable(_ peerID: PeerID) -> Bool { false }
|
||||||
func peerNickname(peerID: String) -> String? { nil }
|
func peerNickname(peerID: PeerID) -> String? { nil }
|
||||||
func getPeerNicknames() -> [String : String] { [:] }
|
func getPeerNicknames() -> [PeerID : String] { [:] }
|
||||||
|
|
||||||
func getFingerprint(for peerID: String) -> String? { nil }
|
func getFingerprint(for peerID: PeerID) -> String? { nil }
|
||||||
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState { .none }
|
func getNoiseSessionState(for peerID: PeerID) -> LazyHandshakeState { .none }
|
||||||
func triggerHandshake(with peerID: String) { /* no-op */ }
|
func triggerHandshake(with peerID: PeerID) { /* no-op */ }
|
||||||
|
|
||||||
// Nostr does not use Noise sessions here; return a cached placeholder to avoid reallocation
|
// Nostr does not use Noise sessions here; return a cached placeholder to avoid reallocation
|
||||||
private static var cachedNoiseService: NoiseEncryptionService?
|
private static var cachedNoiseService: NoiseEncryptionService?
|
||||||
@@ -59,11 +62,11 @@ final class NostrTransport: Transport {
|
|||||||
// Public broadcast not supported over Nostr here
|
// Public broadcast not supported over Nostr here
|
||||||
func sendMessage(_ content: String, mentions: [String]) { /* no-op */ }
|
func sendMessage(_ content: String, mentions: [String]) { /* no-op */ }
|
||||||
|
|
||||||
func sendPrivateMessage(_ content: String, to peerID: String, recipientNickname: String, messageID: String) {
|
func sendPrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||||
SecureLogger.debug("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… for peerID \(peerID.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
SecureLogger.debug("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… for peerID \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||||
// Convert recipient npub -> hex (x-only)
|
// Convert recipient npub -> hex (x-only)
|
||||||
let recipientHex: String
|
let recipientHex: String
|
||||||
do {
|
do {
|
||||||
@@ -77,7 +80,7 @@ final class NostrTransport: Transport {
|
|||||||
SecureLogger.error("NostrTransport: failed to decode npub -> hex: \(error)", category: .session)
|
SecureLogger.error("NostrTransport: failed to decode npub -> hex: \(error)", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
|
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: messageID, recipientPeerID: peerID.id, senderPeerID: senderPeerID.id) else {
|
||||||
SecureLogger.error("NostrTransport: failed to embed PM packet", category: .session)
|
SecureLogger.error("NostrTransport: failed to embed PM packet", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -90,12 +93,113 @@ final class NostrTransport: Transport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
|
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) {
|
||||||
// Enqueue and process with throttling to avoid relay rate limits
|
// Enqueue and process with throttling to avoid relay rate limits
|
||||||
readQueue.append(QueuedRead(receipt: receipt, peerID: peerID))
|
readQueue.append(QueuedRead(receipt: receipt, peerID: peerID))
|
||||||
processReadQueueIfNeeded()
|
processReadQueueIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
||||||
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||||
|
let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)"
|
||||||
|
SecureLogger.debug("NostrTransport: preparing FAVORITE(\(isFavorite)) to \(recipientNpub.prefix(16))…", category: .session)
|
||||||
|
// Convert recipient npub -> hex
|
||||||
|
let recipientHex: String
|
||||||
|
do {
|
||||||
|
let (hrp, data) = try Bech32.decode(recipientNpub)
|
||||||
|
guard hrp == "npub" else { return }
|
||||||
|
recipientHex = data.hexEncodedString()
|
||||||
|
} catch { return }
|
||||||
|
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: UUID().uuidString, recipientPeerID: peerID.id, senderPeerID: senderPeerID.id) else {
|
||||||
|
SecureLogger.error("NostrTransport: failed to embed favorite notification", category: .session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
|
||||||
|
SecureLogger.error("NostrTransport: failed to build Nostr event for favorite notification", category: .session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SecureLogger.debug("NostrTransport: sending favorite giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||||
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendBroadcastAnnounce() { /* no-op for Nostr */ }
|
||||||
|
func sendDeliveryAck(for messageID: String, to peerID: PeerID) {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
||||||
|
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
||||||
|
SecureLogger.debug("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))…", category: .session)
|
||||||
|
let recipientHex: String
|
||||||
|
do {
|
||||||
|
let (hrp, data) = try Bech32.decode(recipientNpub)
|
||||||
|
guard hrp == "npub" else { return }
|
||||||
|
recipientHex = data.hexEncodedString()
|
||||||
|
} catch { return }
|
||||||
|
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .delivered, messageID: messageID, recipientPeerID: peerID.id, senderPeerID: senderPeerID.id) else {
|
||||||
|
SecureLogger.error("NostrTransport: failed to embed DELIVERED ack", category: .session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
|
||||||
|
SecureLogger.error("NostrTransport: failed to build Nostr event for DELIVERED ack", category: .session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SecureLogger.debug("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||||
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Geohash Helpers
|
||||||
|
|
||||||
|
extension NostrTransport {
|
||||||
|
|
||||||
|
// MARK: Geohash ACK helpers
|
||||||
|
func sendDeliveryAckGeohash(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {
|
||||||
|
Task { @MainActor in
|
||||||
|
SecureLogger.debug("GeoDM: send DELIVERED -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
||||||
|
guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .delivered, messageID: messageID, senderPeerID: senderPeerID.id) else { return }
|
||||||
|
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return }
|
||||||
|
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
||||||
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendReadReceiptGeohash(_ messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {
|
||||||
|
Task { @MainActor in
|
||||||
|
SecureLogger.debug("GeoDM: send READ -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
||||||
|
guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .readReceipt, messageID: messageID, senderPeerID: senderPeerID.id) else { return }
|
||||||
|
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return }
|
||||||
|
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
||||||
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Geohash DMs (per-geohash identity)
|
||||||
|
func sendPrivateMessageGeohash(content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String) {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard !recipientHex.isEmpty else { return }
|
||||||
|
SecureLogger.debug("GeoDM: send PM -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
||||||
|
// Build embedded BitChat packet without recipient peer ID
|
||||||
|
guard let embedded = NostrEmbeddedBitChat.encodePMForNostrNoRecipient(content: content, messageID: messageID, senderPeerID: senderPeerID.id) else {
|
||||||
|
SecureLogger.error("NostrTransport: failed to embed geohash PM packet", category: .session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else {
|
||||||
|
SecureLogger.error("NostrTransport: failed to build Nostr event for geohash PM", category: .session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SecureLogger.debug("NostrTransport: sending geohash PM giftWrap id=\(event.id.prefix(16))…", category: .session)
|
||||||
|
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
||||||
|
NostrRelayManager.shared.sendEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
|
extension NostrTransport {
|
||||||
private func processReadQueueIfNeeded() {
|
private func processReadQueueIfNeeded() {
|
||||||
guard !isSendingReadAcks else { return }
|
guard !isSendingReadAcks else { return }
|
||||||
guard !readQueue.isEmpty else { return }
|
guard !readQueue.isEmpty else { return }
|
||||||
@@ -117,7 +221,7 @@ final class NostrTransport: Transport {
|
|||||||
guard hrp == "npub" else { scheduleNextReadAck(); return }
|
guard hrp == "npub" else { scheduleNextReadAck(); return }
|
||||||
recipientHex = data.hexEncodedString()
|
recipientHex = data.hexEncodedString()
|
||||||
} catch { scheduleNextReadAck(); return }
|
} catch { scheduleNextReadAck(); return }
|
||||||
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .readReceipt, messageID: item.receipt.originalMessageID, recipientPeerID: item.peerID, senderPeerID: senderPeerID) else {
|
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .readReceipt, messageID: item.receipt.originalMessageID, recipientPeerID: item.peerID.id, senderPeerID: senderPeerID.id) else {
|
||||||
SecureLogger.error("NostrTransport: failed to embed READ ack", category: .session)
|
SecureLogger.error("NostrTransport: failed to embed READ ack", category: .session)
|
||||||
scheduleNextReadAck(); return
|
scheduleNextReadAck(); return
|
||||||
}
|
}
|
||||||
@@ -139,111 +243,18 @@ final class NostrTransport: Transport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
|
|
||||||
Task { @MainActor in
|
|
||||||
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
|
||||||
let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)"
|
|
||||||
SecureLogger.debug("NostrTransport: preparing FAVORITE(\(isFavorite)) to \(recipientNpub.prefix(16))…", category: .session)
|
|
||||||
// Convert recipient npub -> hex
|
|
||||||
let recipientHex: String
|
|
||||||
do {
|
|
||||||
let (hrp, data) = try Bech32.decode(recipientNpub)
|
|
||||||
guard hrp == "npub" else { return }
|
|
||||||
recipientHex = data.hexEncodedString()
|
|
||||||
} catch { return }
|
|
||||||
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: UUID().uuidString, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
|
|
||||||
SecureLogger.error("NostrTransport: failed to embed favorite notification", category: .session)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
|
|
||||||
SecureLogger.error("NostrTransport: failed to build Nostr event for favorite notification", category: .session)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
SecureLogger.debug("NostrTransport: sending favorite giftWrap id=\(event.id.prefix(16))…", category: .session)
|
|
||||||
NostrRelayManager.shared.sendEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helpers
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func resolveRecipientNpub(for peerID: String) -> String? {
|
private func resolveRecipientNpub(for peerID: PeerID) -> String? {
|
||||||
if let noiseKey = Data(hexString: peerID),
|
if let noiseKey = Data(hexString: peerID.id),
|
||||||
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
||||||
let npub = fav.peerNostrPublicKey {
|
let npub = fav.peerNostrPublicKey {
|
||||||
return npub
|
return npub
|
||||||
}
|
}
|
||||||
if peerID.count == 16,
|
if peerID.id.count == 16,
|
||||||
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: PeerID(str: peerID)),
|
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID),
|
||||||
let npub = fav.peerNostrPublicKey {
|
let npub = fav.peerNostrPublicKey {
|
||||||
return npub
|
return npub
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendBroadcastAnnounce() { /* no-op for Nostr */ }
|
|
||||||
func sendDeliveryAck(for messageID: String, to peerID: String) {
|
|
||||||
Task { @MainActor in
|
|
||||||
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
|
|
||||||
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
|
|
||||||
SecureLogger.debug("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))…", category: .session)
|
|
||||||
let recipientHex: String
|
|
||||||
do {
|
|
||||||
let (hrp, data) = try Bech32.decode(recipientNpub)
|
|
||||||
guard hrp == "npub" else { return }
|
|
||||||
recipientHex = data.hexEncodedString()
|
|
||||||
} catch { return }
|
|
||||||
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .delivered, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
|
|
||||||
SecureLogger.error("NostrTransport: failed to embed DELIVERED ack", category: .session)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
|
|
||||||
SecureLogger.error("NostrTransport: failed to build Nostr event for DELIVERED ack", category: .session)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
SecureLogger.debug("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))…", category: .session)
|
|
||||||
NostrRelayManager.shared.sendEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Geohash ACK helpers
|
|
||||||
func sendDeliveryAckGeohash(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {
|
|
||||||
Task { @MainActor in
|
|
||||||
SecureLogger.debug("GeoDM: send DELIVERED -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
|
||||||
guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .delivered, messageID: messageID, senderPeerID: senderPeerID) else { return }
|
|
||||||
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return }
|
|
||||||
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
|
||||||
NostrRelayManager.shared.sendEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendReadReceiptGeohash(_ messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {
|
|
||||||
Task { @MainActor in
|
|
||||||
SecureLogger.debug("GeoDM: send READ -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
|
||||||
guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .readReceipt, messageID: messageID, senderPeerID: senderPeerID) else { return }
|
|
||||||
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return }
|
|
||||||
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
|
||||||
NostrRelayManager.shared.sendEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Geohash DMs (per-geohash identity)
|
|
||||||
func sendPrivateMessageGeohash(content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String) {
|
|
||||||
Task { @MainActor in
|
|
||||||
guard !recipientHex.isEmpty else { return }
|
|
||||||
SecureLogger.debug("GeoDM: send PM -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))…", category: .session)
|
|
||||||
// Build embedded BitChat packet without recipient peer ID
|
|
||||||
guard let embedded = NostrEmbeddedBitChat.encodePMForNostrNoRecipient(content: content, messageID: messageID, senderPeerID: senderPeerID) else {
|
|
||||||
SecureLogger.error("NostrTransport: failed to embed geohash PM packet", category: .session)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else {
|
|
||||||
SecureLogger.error("NostrTransport: failed to build Nostr event for geohash PM", category: .session)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
SecureLogger.debug("NostrTransport: sending geohash PM giftWrap id=\(event.id.prefix(16))…", category: .session)
|
|
||||||
NostrRelayManager.registerPendingGiftWrap(id: event.id)
|
|
||||||
NostrRelayManager.shared.sendEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// NotificationStreamAssembler.swift
|
||||||
|
// bitchat
|
||||||
|
//
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
// For more information, see <https://unlicense.org>
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct NotificationStreamAssembler {
|
||||||
|
private var buffer = Data()
|
||||||
|
|
||||||
|
mutating func append(_ chunk: Data) -> (frames: [Data], droppedPrefixes: [UInt8], reset: Bool) {
|
||||||
|
guard !chunk.isEmpty else { return ([], [], false) }
|
||||||
|
|
||||||
|
buffer.append(chunk)
|
||||||
|
|
||||||
|
var frames: [Data] = []
|
||||||
|
var dropped: [UInt8] = []
|
||||||
|
var reset = false
|
||||||
|
let maxFrameLength = TransportConfig.blePendingWriteBufferCapBytes
|
||||||
|
|
||||||
|
let minHeaderBytes = 14 // version + type + ttl + timestamp(8) + flags + length(2)
|
||||||
|
let minFramePrefix = minHeaderBytes + BinaryProtocol.senderIDSize
|
||||||
|
|
||||||
|
while buffer.count >= minFramePrefix {
|
||||||
|
guard let first = buffer.first else { break }
|
||||||
|
if first != 1 {
|
||||||
|
dropped.append(buffer.removeFirst())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
guard buffer.count >= minHeaderBytes else { break }
|
||||||
|
|
||||||
|
let headerBytes = Array(buffer.prefix(minFramePrefix))
|
||||||
|
guard headerBytes.count == minFramePrefix else { break }
|
||||||
|
|
||||||
|
let flags = headerBytes[11]
|
||||||
|
let hasRecipient = (flags & BinaryProtocol.Flags.hasRecipient) != 0
|
||||||
|
let hasSignature = (flags & BinaryProtocol.Flags.hasSignature) != 0
|
||||||
|
let payloadLen = (Int(headerBytes[12]) << 8) | Int(headerBytes[13])
|
||||||
|
|
||||||
|
var frameLength = minFramePrefix + payloadLen
|
||||||
|
if hasRecipient { frameLength += BinaryProtocol.recipientIDSize }
|
||||||
|
if hasSignature { frameLength += BinaryProtocol.signatureSize }
|
||||||
|
|
||||||
|
guard frameLength > 0, frameLength <= maxFrameLength else {
|
||||||
|
buffer.removeAll()
|
||||||
|
reset = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if buffer.count < frameLength {
|
||||||
|
// Check if a new frame start exists within the incomplete buffer; if so, drop leading partial bytes.
|
||||||
|
if let nextStart = buffer.dropFirst().firstIndex(of: 1) {
|
||||||
|
let dropCount = buffer.distance(from: buffer.startIndex, to: nextStart)
|
||||||
|
if dropCount > 0 {
|
||||||
|
buffer.removeFirst(dropCount)
|
||||||
|
dropped.append(1) // treat as dropped partial start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame = Data(buffer.prefix(frameLength))
|
||||||
|
frames.append(frame)
|
||||||
|
buffer.removeFirst(frameLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !buffer.isEmpty, buffer.allSatisfy({ $0 == 0 }) {
|
||||||
|
buffer.removeAll(keepingCapacity: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (frames, dropped, reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func reset() {
|
||||||
|
buffer.removeAll(keepingCapacity: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ final class PrivateChatManager: ObservableObject {
|
|||||||
selectedPeer = peerID
|
selectedPeer = peerID
|
||||||
|
|
||||||
// Store fingerprint for persistence across reconnections
|
// Store fingerprint for persistence across reconnections
|
||||||
if let fingerprint = meshService?.getFingerprint(for: peerID) {
|
if let fingerprint = meshService?.getFingerprint(for: PeerID(str: peerID)) {
|
||||||
selectedPeerFingerprint = fingerprint
|
selectedPeerFingerprint = fingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ final class PrivateChatManager: ObservableObject {
|
|||||||
// Create read receipt using the simplified method
|
// Create read receipt using the simplified method
|
||||||
let receipt = ReadReceipt(
|
let receipt = ReadReceipt(
|
||||||
originalMessageID: message.id,
|
originalMessageID: message.id,
|
||||||
readerID: meshService?.myPeerID ?? "",
|
readerID: meshService?.myPeerID.id ?? "",
|
||||||
readerNickname: meshService?.myNickname ?? ""
|
readerNickname: meshService?.myNickname ?? ""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ final class PrivateChatManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: preserve previous behavior
|
// Fallback: preserve previous behavior
|
||||||
meshService?.sendReadReceipt(receipt, to: senderPeerID.id)
|
meshService?.sendReadReceipt(receipt, to: senderPeerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,17 @@ struct TransportPeerSnapshot: Equatable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protocol Transport: AnyObject {
|
protocol Transport: AnyObject {
|
||||||
// Peer events (preferred over publishers for UI)
|
|
||||||
var peerEventsDelegate: TransportPeerEventsDelegate? { get set }
|
|
||||||
// Event sink
|
// Event sink
|
||||||
var delegate: BitchatDelegate? { get set }
|
var delegate: BitchatDelegate? { get set }
|
||||||
|
// Peer events (preferred over publishers for UI)
|
||||||
|
var peerEventsDelegate: TransportPeerEventsDelegate? { get set }
|
||||||
|
|
||||||
|
// Peer snapshots (for non-UI services)
|
||||||
|
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> { get }
|
||||||
|
func currentPeerSnapshots() -> [TransportPeerSnapshot]
|
||||||
|
|
||||||
// Identity
|
// Identity
|
||||||
var myPeerID: String { get }
|
var myPeerID: PeerID { get }
|
||||||
var myNickname: String { get }
|
var myNickname: String { get }
|
||||||
func setNickname(_ nickname: String)
|
func setNickname(_ nickname: String)
|
||||||
|
|
||||||
@@ -28,37 +32,33 @@ protocol Transport: AnyObject {
|
|||||||
func emergencyDisconnectAll()
|
func emergencyDisconnectAll()
|
||||||
|
|
||||||
// Connectivity and peers
|
// Connectivity and peers
|
||||||
func isPeerConnected(_ peerID: String) -> Bool
|
func isPeerConnected(_ peerID: PeerID) -> Bool
|
||||||
func isPeerReachable(_ peerID: String) -> Bool
|
func isPeerReachable(_ peerID: PeerID) -> Bool
|
||||||
func peerNickname(peerID: String) -> String?
|
func peerNickname(peerID: PeerID) -> String?
|
||||||
func getPeerNicknames() -> [String: String]
|
func getPeerNicknames() -> [PeerID: String]
|
||||||
|
|
||||||
// Protocol utilities
|
// Protocol utilities
|
||||||
func getFingerprint(for peerID: String) -> String?
|
func getFingerprint(for peerID: PeerID) -> String?
|
||||||
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState
|
func getNoiseSessionState(for peerID: PeerID) -> LazyHandshakeState
|
||||||
func triggerHandshake(with peerID: String)
|
func triggerHandshake(with peerID: PeerID)
|
||||||
func getNoiseService() -> NoiseEncryptionService
|
func getNoiseService() -> NoiseEncryptionService
|
||||||
|
|
||||||
// Messaging
|
// Messaging
|
||||||
func sendMessage(_ content: String, mentions: [String])
|
func sendMessage(_ content: String, mentions: [String])
|
||||||
func sendPrivateMessage(_ content: String, to peerID: String, recipientNickname: String, messageID: String)
|
func sendPrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String)
|
||||||
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String)
|
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID)
|
||||||
func sendFavoriteNotification(to peerID: String, isFavorite: Bool)
|
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool)
|
||||||
func sendBroadcastAnnounce()
|
func sendBroadcastAnnounce()
|
||||||
func sendDeliveryAck(for messageID: String, to peerID: String)
|
func sendDeliveryAck(for messageID: String, to peerID: PeerID)
|
||||||
|
|
||||||
// QR verification (optional for transports)
|
// QR verification (optional for transports)
|
||||||
func sendVerifyChallenge(to peerID: String, noiseKeyHex: String, nonceA: Data)
|
func sendVerifyChallenge(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
|
||||||
func sendVerifyResponse(to peerID: String, noiseKeyHex: String, nonceA: Data)
|
func sendVerifyResponse(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
|
||||||
|
|
||||||
// Peer snapshots (for non-UI services)
|
|
||||||
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> { get }
|
|
||||||
func currentPeerSnapshots() -> [TransportPeerSnapshot]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Transport {
|
extension Transport {
|
||||||
func sendVerifyChallenge(to peerID: String, noiseKeyHex: String, nonceA: Data) {}
|
func sendVerifyChallenge(to peerID: PeerID, noiseKeyHex: String, nonceA: Data) {}
|
||||||
func sendVerifyResponse(to peerID: String, noiseKeyHex: String, nonceA: Data) {}
|
func sendVerifyResponse(to peerID: PeerID, noiseKeyHex: String, nonceA: Data) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol TransportPeerEventsDelegate: AnyObject {
|
protocol TransportPeerEventsDelegate: AnyObject {
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
|
|
||||||
if actualNickname.isEmpty {
|
if actualNickname.isEmpty {
|
||||||
// Try to get from mesh service's current peer list
|
// Try to get from mesh service's current peer list
|
||||||
if let meshPeerNickname = meshService.peerNickname(peerID: peerID) {
|
if let meshPeerNickname = meshService.peerNickname(peerID: PeerID(str: peerID)) {
|
||||||
actualNickname = meshPeerNickname
|
actualNickname = meshPeerNickname
|
||||||
SecureLogger.debug("🔍 Got nickname from mesh service: '\(actualNickname)'", category: .session)
|
SecureLogger.debug("🔍 Got nickname from mesh service: '\(actualNickname)'", category: .session)
|
||||||
}
|
}
|
||||||
@@ -309,7 +309,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
router.sendFavoriteNotification(to: PeerID(str: peerID), isFavorite: !wasFavorite)
|
router.sendFavoriteNotification(to: PeerID(str: peerID), isFavorite: !wasFavorite)
|
||||||
} else {
|
} else {
|
||||||
// Fallback to mesh-only if router not yet wired
|
// Fallback to mesh-only if router not yet wired
|
||||||
meshService.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite)
|
meshService.sendFavoriteNotification(to: PeerID(str: peerID), isFavorite: !wasFavorite)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force update of peers to reflect the change
|
// Force update of peers to reflect the change
|
||||||
@@ -360,7 +360,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to get from mesh service
|
// Try to get from mesh service
|
||||||
if let fingerprint = meshService.getFingerprint(for: peerID) {
|
if let fingerprint = meshService.getFingerprint(for: PeerID(str: peerID)) {
|
||||||
fingerprintCache[peerID] = fingerprint
|
fingerprintCache[peerID] = fingerprint
|
||||||
return fingerprint
|
return fingerprint
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ final class GossipSyncManager {
|
|||||||
var gcsTargetFpr: Double = 0.01 // 1%
|
var gcsTargetFpr: Double = 0.01 // 1%
|
||||||
}
|
}
|
||||||
|
|
||||||
private let myPeerID: String
|
private let myPeerID: PeerID
|
||||||
private let config: Config
|
private let config: Config
|
||||||
weak var delegate: Delegate?
|
weak var delegate: Delegate?
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ final class GossipSyncManager {
|
|||||||
private var periodicTimer: DispatchSourceTimer?
|
private var periodicTimer: DispatchSourceTimer?
|
||||||
private let queue = DispatchQueue(label: "mesh.sync", qos: .utility)
|
private let queue = DispatchQueue(label: "mesh.sync", qos: .utility)
|
||||||
|
|
||||||
init(myPeerID: String, config: Config = Config()) {
|
init(myPeerID: PeerID, config: Config = Config()) {
|
||||||
self.myPeerID = myPeerID
|
self.myPeerID = myPeerID
|
||||||
self.config = config
|
self.config = config
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ final class GossipSyncManager {
|
|||||||
let payload = buildGcsPayload()
|
let payload = buildGcsPayload()
|
||||||
let pkt = BitchatPacket(
|
let pkt = BitchatPacket(
|
||||||
type: MessageType.requestSync.rawValue,
|
type: MessageType.requestSync.rawValue,
|
||||||
senderID: Data(hexString: myPeerID) ?? Data(),
|
senderID: Data(hexString: myPeerID.id) ?? Data(),
|
||||||
recipientID: nil, // broadcast
|
recipientID: nil, // broadcast
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: payload,
|
payload: payload,
|
||||||
@@ -112,7 +112,7 @@ final class GossipSyncManager {
|
|||||||
}
|
}
|
||||||
let pkt = BitchatPacket(
|
let pkt = BitchatPacket(
|
||||||
type: MessageType.requestSync.rawValue,
|
type: MessageType.requestSync.rawValue,
|
||||||
senderID: Data(hexString: myPeerID) ?? Data(),
|
senderID: Data(hexString: myPeerID.id) ?? Data(),
|
||||||
recipientID: recipient,
|
recipientID: recipient,
|
||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: payload,
|
payload: payload,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ struct PeerDisplayNameResolver {
|
|||||||
/// - peers: Array of tuples (id, nickname, isConnected).
|
/// - peers: Array of tuples (id, nickname, isConnected).
|
||||||
/// - selfNickname: The local user's current nickname, included in collision counts to suffix remotes matching it.
|
/// - selfNickname: The local user's current nickname, included in collision counts to suffix remotes matching it.
|
||||||
/// - Returns: Map of peerID -> displayName.
|
/// - Returns: Map of peerID -> displayName.
|
||||||
static func resolve(_ peers: [(id: String, nickname: String, isConnected: Bool)], selfNickname: String) -> [String: String] {
|
static func resolve(_ peers: [(id: String, nickname: String, isConnected: Bool)], selfNickname: String) -> [PeerID: String] {
|
||||||
// Count collisions among connected peers and include our own nickname
|
// Count collisions among connected peers and include our own nickname
|
||||||
var counts: [String: Int] = [:]
|
var counts: [String: Int] = [:]
|
||||||
for p in peers where p.isConnected {
|
for p in peers where p.isConnected {
|
||||||
@@ -15,13 +15,13 @@ struct PeerDisplayNameResolver {
|
|||||||
}
|
}
|
||||||
counts[selfNickname, default: 0] += 1
|
counts[selfNickname, default: 0] += 1
|
||||||
|
|
||||||
var result: [String: String] = [:]
|
var result: [PeerID: String] = [:]
|
||||||
for p in peers {
|
for p in peers {
|
||||||
var name = p.nickname
|
var name = p.nickname
|
||||||
if p.isConnected, (counts[p.nickname] ?? 0) > 1 {
|
if p.isConnected, (counts[p.nickname] ?? 0) > 1 {
|
||||||
name += "#" + String(p.id.prefix(4))
|
name += "#" + String(p.id.prefix(4))
|
||||||
}
|
}
|
||||||
result[p.id] = name
|
result[PeerID(str: p.id)] = name
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1192,7 +1192,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the peer's nickname to check for temporary Nostr peer IDs
|
// Get the peer's nickname to check for temporary Nostr peer IDs
|
||||||
let peerNickname = meshService.peerNickname(peerID: peerID)?.lowercased() ?? ""
|
let peerNickname = meshService.peerNickname(peerID: PeerID(str: peerID))?.lowercased() ?? ""
|
||||||
|
|
||||||
// Check if any temporary Nostr peer IDs have unread messages from this nickname
|
// Check if any temporary Nostr peer IDs have unread messages from this nickname
|
||||||
for unreadPeerID in unreadPrivateMessages {
|
for unreadPeerID in unreadPrivateMessages {
|
||||||
@@ -1416,7 +1416,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let myGeoIdentity = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) {
|
let myGeoIdentity = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) {
|
||||||
let suffix = String(myGeoIdentity.publicKeyHex.suffix(4))
|
let suffix = String(myGeoIdentity.publicKeyHex.suffix(4))
|
||||||
displaySender = nickname + "#" + suffix
|
displaySender = nickname + "#" + suffix
|
||||||
localSenderPeerID = "nostr:\(myGeoIdentity.publicKeyHex.prefix(TransportConfig.nostrShortKeyDisplayLength))"
|
localSenderPeerID = PeerID(nostr: myGeoIdentity.publicKeyHex)
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = BitchatMessage(
|
let message = BitchatMessage(
|
||||||
@@ -1424,7 +1424,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
content: trimmed,
|
content: trimmed,
|
||||||
timestamp: Date(),
|
timestamp: Date(),
|
||||||
isRelay: false,
|
isRelay: false,
|
||||||
senderPeerID: localSenderPeerID,
|
senderPeerID: localSenderPeerID.id,
|
||||||
mentions: mentions.isEmpty ? nil : mentions
|
mentions: mentions.isEmpty ? nil : mentions
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2198,7 +2198,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Check if blocked
|
// Check if blocked
|
||||||
if unifiedPeerService.isBlocked(peerID) {
|
if unifiedPeerService.isBlocked(peerID) {
|
||||||
let nickname = meshService.peerNickname(peerID: peerID) ?? "user"
|
let nickname = meshService.peerNickname(peerID: PeerID(str: peerID)) ?? "user"
|
||||||
addSystemMessage(
|
addSystemMessage(
|
||||||
String(
|
String(
|
||||||
format: String(localized: "system.dm.blocked_recipient", comment: "System message when attempting to message a blocked user"),
|
format: String(localized: "system.dm.blocked_recipient", comment: "System message when attempting to message a blocked user"),
|
||||||
@@ -2216,14 +2216,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Determine routing method and recipient nickname
|
// Determine routing method and recipient nickname
|
||||||
guard let noiseKey = Data(hexString: peerID) else { return }
|
guard let noiseKey = Data(hexString: peerID) else { return }
|
||||||
let isConnected = meshService.isPeerConnected(peerID)
|
let isConnected = meshService.isPeerConnected(PeerID(str: peerID))
|
||||||
let isReachable = meshService.isPeerReachable(peerID)
|
let isReachable = meshService.isPeerReachable(PeerID(str: peerID))
|
||||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey)
|
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey)
|
||||||
let isMutualFavorite = favoriteStatus?.isMutual ?? false
|
let isMutualFavorite = favoriteStatus?.isMutual ?? false
|
||||||
let hasNostrKey = favoriteStatus?.peerNostrPublicKey != nil
|
let hasNostrKey = favoriteStatus?.peerNostrPublicKey != nil
|
||||||
|
|
||||||
// Get nickname from various sources
|
// Get nickname from various sources
|
||||||
var recipientNickname = meshService.peerNickname(peerID: peerID)
|
var recipientNickname = meshService.peerNickname(peerID: PeerID(str: peerID))
|
||||||
if recipientNickname == nil && favoriteStatus != nil {
|
if recipientNickname == nil && favoriteStatus != nil {
|
||||||
recipientNickname = favoriteStatus?.peerNickname
|
recipientNickname = favoriteStatus?.peerNickname
|
||||||
}
|
}
|
||||||
@@ -2242,7 +2242,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
recipientNickname: recipientNickname,
|
recipientNickname: recipientNickname,
|
||||||
senderPeerID: meshService.myPeerID,
|
senderPeerID: meshService.myPeerID.id,
|
||||||
mentions: nil,
|
mentions: nil,
|
||||||
deliveryStatus: .sending
|
deliveryStatus: .sending
|
||||||
)
|
)
|
||||||
@@ -2299,7 +2299,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
isRelay: false,
|
isRelay: false,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
recipientNickname: nickname,
|
recipientNickname: nickname,
|
||||||
senderPeerID: meshService.myPeerID,
|
senderPeerID: meshService.myPeerID.id,
|
||||||
deliveryStatus: .sending
|
deliveryStatus: .sending
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2396,8 +2396,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
isRelay: false,
|
isRelay: false,
|
||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
recipientNickname: meshService.peerNickname(peerID: peerID),
|
recipientNickname: meshService.peerNickname(peerID: PeerID(str: peerID)),
|
||||||
senderPeerID: meshService.myPeerID
|
senderPeerID: meshService.myPeerID.id
|
||||||
)
|
)
|
||||||
if privateChats[peerID] == nil { privateChats[peerID] = [] }
|
if privateChats[peerID] == nil { privateChats[peerID] = [] }
|
||||||
privateChats[peerID]?.append(systemMessage)
|
privateChats[peerID]?.append(systemMessage)
|
||||||
@@ -2446,7 +2446,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let peerNickname = meshService.peerNickname(peerID: peerID) ?? "unknown"
|
let peerNickname = meshService.peerNickname(peerID: PeerID(str: peerID)) ?? "unknown"
|
||||||
|
|
||||||
// Check if the peer is blocked
|
// Check if the peer is blocked
|
||||||
if unifiedPeerService.isBlocked(peerID) {
|
if unifiedPeerService.isBlocked(peerID) {
|
||||||
@@ -2506,7 +2506,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
originalSender: message.originalSender,
|
originalSender: message.originalSender,
|
||||||
isPrivate: message.isPrivate,
|
isPrivate: message.isPrivate,
|
||||||
recipientNickname: message.recipientNickname,
|
recipientNickname: message.recipientNickname,
|
||||||
senderPeerID: message.senderPeerID == meshService.myPeerID ? meshService.myPeerID : peerID, // Update peer ID if it's from them
|
senderPeerID: message.senderPeerID == meshService.myPeerID ? meshService.myPeerID.id : peerID, // Update peer ID if it's from them
|
||||||
mentions: message.mentions,
|
mentions: message.mentions,
|
||||||
deliveryStatus: message.deliveryStatus
|
deliveryStatus: message.deliveryStatus
|
||||||
)
|
)
|
||||||
@@ -2624,10 +2624,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Trigger handshake if needed (mesh peers only). Skip for Nostr geohash conv keys.
|
// Trigger handshake if needed (mesh peers only). Skip for Nostr geohash conv keys.
|
||||||
if !peerID.hasPrefix("nostr_") && !peerID.hasPrefix("nostr:") {
|
if !peerID.hasPrefix("nostr_") && !peerID.hasPrefix("nostr:") {
|
||||||
let sessionState = meshService.getNoiseSessionState(for: peerID)
|
let sessionState = meshService.getNoiseSessionState(for: PeerID(str: peerID))
|
||||||
switch sessionState {
|
switch sessionState {
|
||||||
case .none, .failed:
|
case .none, .failed:
|
||||||
meshService.triggerHandshake(with: peerID)
|
meshService.triggerHandshake(with: PeerID(str: peerID))
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -2799,7 +2799,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Find peer nickname
|
// Find peer nickname
|
||||||
let peerNickname: String
|
let peerNickname: String
|
||||||
if let nickname = meshService.peerNickname(peerID: peerID) {
|
if let nickname = meshService.peerNickname(peerID: PeerID(str: peerID)) {
|
||||||
peerNickname = nickname
|
peerNickname = nickname
|
||||||
} else if let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(for: peerPublicKey) {
|
} else if let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(for: peerPublicKey) {
|
||||||
peerNickname = favorite.peerNickname
|
peerNickname = favorite.peerNickname
|
||||||
@@ -2864,10 +2864,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
if let peerID = selectedPrivateChatPeer {
|
if let peerID = selectedPrivateChatPeer {
|
||||||
// In private chat - send to the other person
|
// In private chat - send to the other person
|
||||||
if let peerNickname = meshService.peerNickname(peerID: peerID) {
|
if let peerNickname = meshService.peerNickname(peerID: PeerID(str: peerID)) {
|
||||||
// Only send screenshot notification if we have an established session
|
// Only send screenshot notification if we have an established session
|
||||||
// This prevents triggering handshake requests for screenshot notifications
|
// This prevents triggering handshake requests for screenshot notifications
|
||||||
let sessionState = meshService.getNoiseSessionState(for: peerID)
|
let sessionState = meshService.getNoiseSessionState(for: PeerID(str: peerID))
|
||||||
switch sessionState {
|
switch sessionState {
|
||||||
case .established:
|
case .established:
|
||||||
// Send the message directly without going through sendPrivateMessage to avoid local echo
|
// Send the message directly without going through sendPrivateMessage to avoid local echo
|
||||||
@@ -2886,8 +2886,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
isRelay: false,
|
isRelay: false,
|
||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
recipientNickname: meshService.peerNickname(peerID: peerID),
|
recipientNickname: meshService.peerNickname(peerID: PeerID(str: peerID)),
|
||||||
senderPeerID: meshService.myPeerID
|
senderPeerID: meshService.myPeerID.id
|
||||||
)
|
)
|
||||||
var chats = privateChats
|
var chats = privateChats
|
||||||
if chats[peerID] == nil {
|
if chats[peerID] == nil {
|
||||||
@@ -2972,7 +2972,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
var actualPeerID = peerID
|
var actualPeerID = peerID
|
||||||
|
|
||||||
// Check if this peer ID exists in current nicknames
|
// Check if this peer ID exists in current nicknames
|
||||||
if meshService.peerNickname(peerID: peerID) == nil {
|
if meshService.peerNickname(peerID: PeerID(str: peerID)) == nil {
|
||||||
// Peer not found with this ID, try to find by fingerprint or nickname
|
// Peer not found with this ID, try to find by fingerprint or nickname
|
||||||
if let oldNoiseKey = Data(hexString: peerID),
|
if let oldNoiseKey = Data(hexString: peerID),
|
||||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: oldNoiseKey) {
|
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: oldNoiseKey) {
|
||||||
@@ -2982,7 +2982,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
for (currentPeerID, currentNickname) in meshService.getPeerNicknames() {
|
for (currentPeerID, currentNickname) in meshService.getPeerNicknames() {
|
||||||
if currentNickname == peerNickname {
|
if currentNickname == peerNickname {
|
||||||
SecureLogger.info("📖 Resolved updated peer ID for read receipt: \(peerID) -> \(currentPeerID)", category: .session)
|
SecureLogger.info("📖 Resolved updated peer ID for read receipt: \(peerID) -> \(currentPeerID)", category: .session)
|
||||||
actualPeerID = currentPeerID
|
actualPeerID = currentPeerID.id
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3054,7 +3054,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if !sentReadReceipts.contains(message.id) {
|
if !sentReadReceipts.contains(message.id) {
|
||||||
// Use stable Noise key hex if available; else fall back to peerID
|
// Use stable Noise key hex if available; else fall back to peerID
|
||||||
let recipPeer = (Data(hexString: peerID) != nil) ? peerID : (unifiedPeerService.getPeer(by: peerID)?.noisePublicKey.hexEncodedString() ?? peerID)
|
let recipPeer = (Data(hexString: peerID) != nil) ? peerID : (unifiedPeerService.getPeer(by: peerID)?.noisePublicKey.hexEncodedString() ?? peerID)
|
||||||
let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID, readerNickname: nickname)
|
let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID.id, readerNickname: nickname)
|
||||||
messageRouter.sendReadReceipt(receipt, to: PeerID(str: recipPeer))
|
messageRouter.sendReadReceipt(receipt, to: PeerID(str: recipPeer))
|
||||||
sentReadReceipts.insert(message.id)
|
sentReadReceipts.insert(message.id)
|
||||||
}
|
}
|
||||||
@@ -3480,7 +3480,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if case .location(let ch) = activeChannel, let id = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) {
|
if case .location(let ch) = activeChannel, let id = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) {
|
||||||
return String(id.publicKeyHex.suffix(4))
|
return String(id.publicKeyHex.suffix(4))
|
||||||
}
|
}
|
||||||
return String(meshService.myPeerID.prefix(4))
|
return String(meshService.myPeerID.id.prefix(4))
|
||||||
}()
|
}()
|
||||||
let isMentionToMe: Bool = {
|
let isMentionToMe: Bool = {
|
||||||
if mBase == nickname {
|
if mBase == nickname {
|
||||||
@@ -3772,7 +3772,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Check if we've ever established a session by looking for a fingerprint
|
// Check if we've ever established a session by looking for a fingerprint
|
||||||
let hasEverEstablishedSession = getFingerprint(for: peerID) != nil
|
let hasEverEstablishedSession = getFingerprint(for: peerID) != nil
|
||||||
|
|
||||||
let sessionState = meshService.getNoiseSessionState(for: peerID)
|
let sessionState = meshService.getNoiseSessionState(for: PeerID(str: peerID))
|
||||||
|
|
||||||
let status: EncryptionStatus
|
let status: EncryptionStatus
|
||||||
|
|
||||||
@@ -4256,7 +4256,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// First try direct peer nicknames from mesh service
|
// First try direct peer nicknames from mesh service
|
||||||
let peerNicknames = meshService.getPeerNicknames()
|
let peerNicknames = meshService.getPeerNicknames()
|
||||||
if let nickname = peerNicknames[peerID] {
|
if let nickname = peerNicknames[PeerID(str: peerID)] {
|
||||||
return nickname
|
return nickname
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4365,7 +4365,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// If a QR verification is pending but not sent yet, send it now that session is authenticated
|
// If a QR verification is pending but not sent yet, send it now that session is authenticated
|
||||||
if var pending = self.pendingQRVerifications[peerID], pending.sent == false {
|
if var pending = self.pendingQRVerifications[peerID], pending.sent == false {
|
||||||
self.meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: pending.noiseKeyHex, nonceA: pending.nonceA)
|
self.meshService.sendVerifyChallenge(to: PeerID(str: peerID), noiseKeyHex: pending.noiseKeyHex, nonceA: pending.nonceA)
|
||||||
pending.sent = true
|
pending.sent = true
|
||||||
self.pendingQRVerifications[peerID] = pending
|
self.pendingQRVerifications[peerID] = pending
|
||||||
SecureLogger.debug("📤 Sent deferred verify challenge to \(peerID) after handshake", category: .security)
|
SecureLogger.debug("📤 Sent deferred verify challenge to \(peerID) after handshake", category: .security)
|
||||||
@@ -4455,7 +4455,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
)
|
)
|
||||||
handlePrivateMessage(msg)
|
handlePrivateMessage(msg)
|
||||||
// Send delivery ACK back over BLE
|
// Send delivery ACK back over BLE
|
||||||
meshService.sendDeliveryAck(for: pm.messageID, to: peerID)
|
meshService.sendDeliveryAck(for: pm.messageID, to: PeerID(str: peerID))
|
||||||
|
|
||||||
case .delivered:
|
case .delivered:
|
||||||
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
||||||
@@ -4501,7 +4501,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
meshService.sendVerifyResponse(to: peerID, noiseKeyHex: tlv.noiseKeyHex, nonceA: tlv.nonceA)
|
meshService.sendVerifyResponse(to: PeerID(str: peerID), noiseKeyHex: tlv.noiseKeyHex, nonceA: tlv.nonceA)
|
||||||
// Silent response: no toast needed on responder
|
// Silent response: no toast needed on responder
|
||||||
case .verifyResponse:
|
case .verifyResponse:
|
||||||
guard let resp = VerificationService.shared.parseVerifyResponse(payload) else { return }
|
guard let resp = VerificationService.shared.parseVerifyResponse(payload) else { return }
|
||||||
@@ -4587,11 +4587,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// If Noise session is established, send immediately; otherwise trigger handshake and send on auth
|
// If Noise session is established, send immediately; otherwise trigger handshake and send on auth
|
||||||
let noise = meshService.getNoiseService()
|
let noise = meshService.getNoiseService()
|
||||||
if noise.hasEstablishedSession(with: PeerID(str: peerID)) {
|
if noise.hasEstablishedSession(with: PeerID(str: peerID)) {
|
||||||
meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: qr.noiseKeyHex, nonceA: nonce)
|
meshService.sendVerifyChallenge(to: PeerID(str: peerID), noiseKeyHex: qr.noiseKeyHex, nonceA: nonce)
|
||||||
pending.sent = true
|
pending.sent = true
|
||||||
pendingQRVerifications[peerID] = pending
|
pendingQRVerifications[peerID] = pending
|
||||||
} else {
|
} else {
|
||||||
meshService.triggerHandshake(with: peerID)
|
meshService.triggerHandshake(with: PeerID(str: peerID))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -4658,7 +4658,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
originalSender: msg.originalSender,
|
originalSender: msg.originalSender,
|
||||||
isPrivate: msg.isPrivate,
|
isPrivate: msg.isPrivate,
|
||||||
recipientNickname: msg.recipientNickname,
|
recipientNickname: msg.recipientNickname,
|
||||||
senderPeerID: (msg.senderPeerID == meshService.myPeerID) ? meshService.myPeerID : stableKeyHex,
|
senderPeerID: msg.senderPeerID == meshService.myPeerID ? meshService.myPeerID.id : stableKeyHex,
|
||||||
mentions: msg.mentions,
|
mentions: msg.mentions,
|
||||||
deliveryStatus: msg.deliveryStatus
|
deliveryStatus: msg.deliveryStatus
|
||||||
)
|
)
|
||||||
@@ -4713,7 +4713,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
self.networkResetTimer = nil
|
self.networkResetTimer = nil
|
||||||
// Count mesh peers that are connected OR recently reachable via mesh relays
|
// Count mesh peers that are connected OR recently reachable via mesh relays
|
||||||
let meshPeers = peers.filter { peerID in
|
let meshPeers = peers.filter { peerID in
|
||||||
self.meshService.isPeerConnected(peerID) || self.meshService.isPeerReachable(peerID)
|
self.meshService.isPeerConnected(PeerID(str: peerID)) || self.meshService.isPeerReachable(PeerID(str: peerID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rising-edge only: previously zero peers, now > 0 peers
|
// Rising-edge only: previously zero peers, now > 0 peers
|
||||||
@@ -4844,7 +4844,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
var validTokens = Set(peerNicknames.values)
|
var validTokens = Set(peerNicknames.values)
|
||||||
// Always allow mentioning self by base nickname and suffixed disambiguator
|
// Always allow mentioning self by base nickname and suffixed disambiguator
|
||||||
validTokens.insert(nickname)
|
validTokens.insert(nickname)
|
||||||
let selfSuffixToken = nickname + "#" + String(meshService.myPeerID.prefix(4))
|
let selfSuffixToken = nickname + "#" + String(meshService.myPeerID.id.prefix(4))
|
||||||
validTokens.insert(selfSuffixToken)
|
validTokens.insert(selfSuffixToken)
|
||||||
|
|
||||||
for match in matches {
|
for match in matches {
|
||||||
@@ -5295,7 +5295,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
if !sentReadReceipts.contains(message.id) {
|
if !sentReadReceipts.contains(message.id) {
|
||||||
if let key {
|
if let key {
|
||||||
let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID, readerNickname: nickname)
|
let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID.id, readerNickname: nickname)
|
||||||
SecureLogger.debug("Viewing chat; sending READ ack for \(message.id.prefix(8))… via router", category: .session)
|
SecureLogger.debug("Viewing chat; sending READ ack for \(message.id.prefix(8))… via router", category: .session)
|
||||||
messageRouter.sendReadReceipt(receipt, to: PeerID(hexData: key))
|
messageRouter.sendReadReceipt(receipt, to: PeerID(hexData: key))
|
||||||
sentReadReceipts.insert(message.id)
|
sentReadReceipts.insert(message.id)
|
||||||
@@ -5636,7 +5636,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try mesh first for connected peers
|
// Try mesh first for connected peers
|
||||||
if meshService.isPeerConnected(peerID) {
|
if meshService.isPeerConnected(PeerID(str: peerID)) {
|
||||||
messageRouter.sendFavoriteNotification(to: PeerID(str: peerID), isFavorite: isFavorite)
|
messageRouter.sendFavoriteNotification(to: PeerID(str: peerID), isFavorite: isFavorite)
|
||||||
SecureLogger.debug("📤 Sent favorite notification via BLE to \(peerID)", category: .session)
|
SecureLogger.debug("📤 Sent favorite notification via BLE to \(peerID)", category: .session)
|
||||||
} else if let key = noiseKey {
|
} else if let key = noiseKey {
|
||||||
@@ -5907,7 +5907,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if !sentReadReceipts.contains(message.id) {
|
if !sentReadReceipts.contains(message.id) {
|
||||||
let receipt = ReadReceipt(
|
let receipt = ReadReceipt(
|
||||||
originalMessageID: message.id,
|
originalMessageID: message.id,
|
||||||
readerID: meshService.myPeerID,
|
readerID: meshService.myPeerID.id,
|
||||||
readerNickname: nickname
|
readerNickname: nickname
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -5918,7 +5918,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if let noiseKey = Data(hexString: recipientID),
|
if let noiseKey = Data(hexString: recipientID),
|
||||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
||||||
favoriteStatus.peerNostrPublicKey != nil,
|
favoriteStatus.peerNostrPublicKey != nil,
|
||||||
self.meshService.peerNickname(peerID: recipientID) == nil {
|
self.meshService.peerNickname(peerID: PeerID(str: recipientID)) == nil {
|
||||||
originalTransport = "nostr"
|
originalTransport = "nostr"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6114,7 +6114,7 @@ private func checkForMentions(_ message: BitchatMessage) {
|
|||||||
let meshPeers = meshService.getPeerNicknames()
|
let meshPeers = meshService.getPeerNicknames()
|
||||||
let collisions = meshPeers.values.filter { $0.hasPrefix(nickname + "#") }
|
let collisions = meshPeers.values.filter { $0.hasPrefix(nickname + "#") }
|
||||||
if !collisions.isEmpty {
|
if !collisions.isEmpty {
|
||||||
let suffix = "#" + String(meshService.myPeerID.prefix(4))
|
let suffix = "#" + String(meshService.myPeerID.id.prefix(4))
|
||||||
myTokens = [nickname + suffix]
|
myTokens = [nickname + suffix]
|
||||||
}
|
}
|
||||||
let isMentioned = (message.mentions?.contains { myTokens.contains($0) } ?? false)
|
let isMentioned = (message.mentions?.contains { myTokens.contains($0) } ?? false)
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ struct ContentView: View {
|
|||||||
selectedMessageSender = viewModel.geohashDisplayName(for: peerID)
|
selectedMessageSender = viewModel.geohashDisplayName(for: peerID)
|
||||||
} else {
|
} else {
|
||||||
// Mesh sender: use current mesh nickname if available; otherwise fall back to last non-system message
|
// Mesh sender: use current mesh nickname if available; otherwise fall back to last non-system message
|
||||||
if let name = viewModel.meshService.peerNickname(peerID: peerID) {
|
if let name = viewModel.meshService.peerNickname(peerID: PeerID(str: peerID)) {
|
||||||
selectedMessageSender = name
|
selectedMessageSender = name
|
||||||
} else {
|
} else {
|
||||||
selectedMessageSender = viewModel.messages.last(where: { $0.senderPeerID == peerID && $0.sender != "system" })?.sender
|
selectedMessageSender = viewModel.messages.last(where: { $0.senderPeerID == peerID && $0.sender != "system" })?.sender
|
||||||
@@ -1401,7 +1401,7 @@ struct ContentView: View {
|
|||||||
// Try mesh/unified peer display
|
// Try mesh/unified peer display
|
||||||
if let name = peer?.displayName { return name }
|
if let name = peer?.displayName { return name }
|
||||||
// Try direct mesh nickname (connected-only)
|
// Try direct mesh nickname (connected-only)
|
||||||
if let name = viewModel.meshService.peerNickname(peerID: headerPeerID) { return name }
|
if let name = viewModel.meshService.peerNickname(peerID: PeerID(str: headerPeerID)) { return name }
|
||||||
// Try favorite nickname by stable Noise key
|
// Try favorite nickname by stable Noise key
|
||||||
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: Data(hexString: headerPeerID) ?? Data()),
|
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: Data(hexString: headerPeerID) ?? Data()),
|
||||||
!fav.peerNickname.isEmpty { return fav.peerNickname }
|
!fav.peerNickname.isEmpty { return fav.peerNickname }
|
||||||
@@ -1477,7 +1477,7 @@ struct ContentView: View {
|
|||||||
// Should not happen for PM header, but handle gracefully
|
// Should not happen for PM header, but handle gracefully
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
} else if viewModel.meshService.isPeerReachable(headerPeerID) {
|
} else if viewModel.meshService.isPeerReachable(PeerID(str: headerPeerID)) {
|
||||||
// Fallback: reachable via mesh but not in current peer list
|
// Fallback: reachable via mesh but not in current peer list
|
||||||
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
|
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
|
||||||
.font(.bitchatSystem(size: 14))
|
.font(.bitchatSystem(size: 14))
|
||||||
@@ -1493,7 +1493,7 @@ struct ContentView: View {
|
|||||||
.accessibilityLabel(
|
.accessibilityLabel(
|
||||||
String(localized: "content.accessibility.available_nostr", comment: "Accessibility label for Nostr-available peer indicator")
|
String(localized: "content.accessibility.available_nostr", comment: "Accessibility label for Nostr-available peer indicator")
|
||||||
)
|
)
|
||||||
} else if viewModel.meshService.isPeerConnected(headerPeerID) || viewModel.connectedPeers.contains(headerPeerID) {
|
} else if viewModel.meshService.isPeerConnected(PeerID(str: headerPeerID)) || viewModel.connectedPeers.contains(headerPeerID) {
|
||||||
// Fallback: if peer lookup is missing but mesh reports connected, show radio
|
// Fallback: if peer lookup is missing but mesh reports connected, show radio
|
||||||
Image(systemName: "dot.radiowaves.left.and.right")
|
Image(systemName: "dot.radiowaves.left.and.right")
|
||||||
.font(.bitchatSystem(size: 14))
|
.font(.bitchatSystem(size: 14))
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ struct FingerprintView: View {
|
|||||||
// Resolve a friendly name
|
// Resolve a friendly name
|
||||||
let peerNickname: String = {
|
let peerNickname: String = {
|
||||||
if let p = viewModel.getPeer(byID: statusPeerID) { return p.displayName }
|
if let p = viewModel.getPeer(byID: statusPeerID) { return p.displayName }
|
||||||
if let name = viewModel.meshService.peerNickname(peerID: statusPeerID) { return name }
|
if let name = viewModel.meshService.peerNickname(peerID: PeerID(str: statusPeerID)) { return name }
|
||||||
if peerID.count == 64, let data = Data(hexString: peerID) {
|
if peerID.count == 64, let data = Data(hexString: peerID) {
|
||||||
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: data), !fav.peerNickname.isEmpty { return fav.peerNickname }
|
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: data), !fav.peerNickname.isEmpty { return fav.peerNickname }
|
||||||
let fp = data.sha256Fingerprint()
|
let fp = data.sha256Fingerprint()
|
||||||
|
|||||||
Reference in New Issue
Block a user