Refactor/ble nostr boundaries (#449)

* Refactor: move Nostr embedding and TLVs out of BLE; add NostrEmbeddedBitChat, Packets, PeerIDUtils; centralize MessageDeduplicator; update ChatViewModel to use new helpers; remove AI_CONTEXT.md and CLAUDE.md

* Rename class to BLEService with compatibility alias; move mention parsing out of BLE; emit low-level BLE events to delegate; unify hex helpers; accept 64-hex in isPeerConnected; add PeerIDResolver

* Project: rename file to BLEService.swift and update Xcode project; keep typealias SimplifiedBluetoothService = BLEService for compatibility

* Remove SimplifiedBluetoothService alias; update app code to use BLEService explicitly

* Tests: rename MockSimplifiedBluetoothService to MockBLEService; update typealiases and Xcode project

* Docs: update comments to refer to BLEService (tests, protocol, noise service)

* Tests: rename SimplifiedBluetoothServiceTests to BLEServiceTests; update project references and class names

* Introduce Transport protocol; BLEService conforms; document delegate-only event pattern in BLEService; keep publishers internal for UnifiedPeerService

* Adopt Transport end-to-end: add TransportPeerSnapshot + publishers; BLEService maps to Transport snapshots; UnifiedPeerService consumes Transport; ChatViewModel holds Transport

* Fix Transport integration: replace getPeerFingerprint with getFingerprint(for:); update PrivateChatManager and CommandProcessor to use Transport; add BLEService.getFingerprint(for:); update PeerManager to use Transport

* Refactor transport and BLE/Nostr layers; unify UI events; fix MainActor isolation

- Rename SimplifiedBluetoothService to BLEService and slim responsibilities
- Introduce Transport protocol and peerEventsDelegate for UI updates
- Add NostrTransport and MessageRouter to route PM/read/favorite via BLE or Nostr
- Centralize TLVs, PeerID utils, and MessageDeduplicator outside BLE
- Update UnifiedPeerService and ChatViewModel to use Transport and delegate events
- Fix MainActor isolation: route delegate calls via Task on MainActor; update notifyUI helper
- Adjust related files and tests accordingly

* BLEService: remove internal publishers; switch to delegate-only events

- Drop legacy messages/peers/fullPeers publishers
- Provide lightweight peerSnapshotSubject only to satisfy Transport
- Rework publishFullPeerData to build snapshots from internal state and notify delegate + subject
- Remove all peersPublisher.send call sites
- Keep UnifiedPeerService on delegate updates exclusively

* Remove inlined Nostr send helpers from ChatViewModel; route via MessageRouter

- Replace direct Nostr sends (PM, ACKs, favorites) with MessageRouter
- Add router method for delivery ACKs and implement NostrTransport.sendDeliveryAck
- Simplify ChatViewModel favorite notification path to use router
- Keep Nostr receive handling intact; reduce duplication

* Fix ReadReceipt initializer usage in ChatViewModel (readerID + readerNickname)

* Fix unused variable warning: replace shadowed 'nostrPubkey' bind with boolean check in ChatViewModel

* Fix queued PM format: use TLV for pending messages after Noise handshake

- Pending messages (including first-time favorite notifications) now use the same TLV encoding as normal sends
- Ensures ChatViewModel can decode on first send, even if handshake completes after queuing

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-08-17 15:17:04 +02:00
committed by GitHub
co-authored by jack
parent 3ebfa85e90
commit 6fbf7eee25
25 changed files with 873 additions and 1647 deletions
+13 -1
View File
@@ -32,7 +32,7 @@
/// 1. **Creation**: Messages are created with type, content, and metadata
/// 2. **Encoding**: Converted to binary format with proper field ordering
/// 3. **Fragmentation**: Split if larger than BLE MTU (512 bytes)
/// 4. **Transmission**: Sent via SimplifiedBluetoothService
/// 4. **Transmission**: Sent via BLEService
/// 5. **Routing**: Relayed by intermediate nodes (TTL decrements)
/// 6. **Reassembly**: Fragments collected and reassembled
/// 7. **Decoding**: Binary data parsed back to message objects
@@ -461,6 +461,10 @@ protocol BitchatDelegate: AnyObject {
func isFavorite(fingerprint: String) -> Bool
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
// Low-level events for better separation of concerns
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date)
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date)
}
// Provide default implementation to make it effectively optional
@@ -472,6 +476,14 @@ extension BitchatDelegate {
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
// Default empty implementation
}
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) {
// Default empty implementation
}
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {
// Default empty implementation
}
}
// MARK: - Noise Payload Helpers
+116
View File
@@ -0,0 +1,116 @@
import Foundation
// MARK: - Protocol TLV Packets
struct AnnouncementPacket {
let nickname: String
let publicKey: Data
private enum TLVType: UInt8 {
case nickname = 0x01
case noisePublicKey = 0x02
}
func encode() -> Data? {
var data = Data()
// TLV for nickname
guard let nicknameData = nickname.data(using: .utf8), nicknameData.count <= 255 else { return nil }
data.append(TLVType.nickname.rawValue)
data.append(UInt8(nicknameData.count))
data.append(nicknameData)
// TLV for public key
guard publicKey.count <= 255 else { return nil }
data.append(TLVType.noisePublicKey.rawValue)
data.append(UInt8(publicKey.count))
data.append(publicKey)
return data
}
static func decode(from data: Data) -> AnnouncementPacket? {
var offset = 0
var nickname: String?
var publicKey: Data?
while offset + 2 <= data.count {
guard let type = TLVType(rawValue: data[offset]) else { return nil }
offset += 1
let length = Int(data[offset])
offset += 1
guard offset + length <= data.count else { return nil }
let value = data[offset..<offset + length]
offset += length
switch type {
case .nickname:
nickname = String(data: value, encoding: .utf8)
case .noisePublicKey:
publicKey = Data(value)
}
}
guard let nickname = nickname, let publicKey = publicKey else { return nil }
return AnnouncementPacket(nickname: nickname, publicKey: publicKey)
}
}
struct PrivateMessagePacket {
let messageID: String
let content: String
private enum TLVType: UInt8 {
case messageID = 0x00
case content = 0x01
}
func encode() -> Data? {
var data = Data()
// TLV for messageID
guard let messageIDData = messageID.data(using: .utf8), messageIDData.count <= 255 else { return nil }
data.append(TLVType.messageID.rawValue)
data.append(UInt8(messageIDData.count))
data.append(messageIDData)
// TLV for content
guard let contentData = content.data(using: .utf8), contentData.count <= 255 else { return nil }
data.append(TLVType.content.rawValue)
data.append(UInt8(contentData.count))
data.append(contentData)
return data
}
static func decode(from data: Data) -> PrivateMessagePacket? {
var offset = 0
var messageID: String?
var content: String?
while offset + 2 <= data.count {
guard let type = TLVType(rawValue: data[offset]) else { return nil }
offset += 1
let length = Int(data[offset])
offset += 1
guard offset + length <= data.count else { return nil }
let value = data[offset..<offset + length]
offset += length
switch type {
case .messageID:
messageID = String(data: value, encoding: .utf8)
case .content:
content = String(data: value, encoding: .utf8)
}
}
guard let messageID = messageID, let content = content else { return nil }
return PrivateMessagePacket(messageID: messageID, content: content)
}
}
+14
View File
@@ -0,0 +1,14 @@
import Foundation
import CryptoKit
// MARK: - Peer ID Utilities
struct PeerIDUtils {
/// Derive the stable 16-hex peer ID from a Noise static public key
static func derivePeerID(fromPublicKey publicKey: Data) -> String {
let digest = SHA256.hash(data: publicKey)
let hex = digest.map { String(format: "%02x", $0) }.joined()
return String(hex.prefix(16))
}
}