PeerID 4/n: BitchatMessage.senderPeerID + String equality (#739)

This commit is contained in:
Islam
2025-10-02 12:57:17 +02:00
committed by GitHub
parent faae625e53
commit 90273cee2d
7 changed files with 94 additions and 21 deletions
+3 -3
View File
@@ -21,7 +21,7 @@ final class BitchatMessage: Codable {
let originalSender: String? let originalSender: String?
let isPrivate: Bool let isPrivate: Bool
let recipientNickname: String? let recipientNickname: String?
let senderPeerID: String? let senderPeerID: PeerID?
let mentions: [String]? // Array of mentioned nicknames let mentions: [String]? // Array of mentioned nicknames
var deliveryStatus: DeliveryStatus? // Delivery tracking var deliveryStatus: DeliveryStatus? // Delivery tracking
@@ -51,7 +51,7 @@ final class BitchatMessage: Codable {
self.originalSender = originalSender self.originalSender = originalSender
self.isPrivate = isPrivate self.isPrivate = isPrivate
self.recipientNickname = recipientNickname self.recipientNickname = recipientNickname
self.senderPeerID = senderPeerID self.senderPeerID = PeerID(str: senderPeerID)
self.mentions = mentions self.mentions = mentions
self.deliveryStatus = deliveryStatus ?? (isPrivate ? .sending : nil) self.deliveryStatus = deliveryStatus ?? (isPrivate ? .sending : nil)
} }
@@ -151,7 +151,7 @@ extension BitchatMessage {
data.append(recipData.prefix(255)) data.append(recipData.prefix(255))
} }
if let senderPeerID = senderPeerID, let peerData = senderPeerID.data(using: .utf8) { if let peerData = senderPeerID?.id.data(using: .utf8) {
data.append(UInt8(min(peerData.count, 255))) data.append(UInt8(min(peerData.count, 255)))
data.append(peerData.prefix(255)) data.append(peerData.prefix(255))
} }
+31
View File
@@ -61,6 +61,12 @@ extension PeerID {
} }
} }
/// Convenience init to handle `Optional<String>`
init?(str: (any StringProtocol)?) {
guard let str else { return nil }
self.init(str: str)
}
/// Convenience init to create PeerID by converting Data to String /// Convenience init to create PeerID by converting Data to String
init?(data: Data) { init?(data: Data) {
guard let str = String(data: data, encoding: .utf8) else { return nil } guard let str = String(data: data, encoding: .utf8) else { return nil }
@@ -175,3 +181,28 @@ extension PeerID: Comparable {
lhs.id < rhs.id lhs.id < rhs.id
} }
} }
// MARK: - String Interop Helpers
// MARK: CustomStringConvertible
extension PeerID: CustomStringConvertible {
/// So it returns the actual `id` like before even inside another String
var description: String {
id
}
}
// MARK: Custom Equatable w/ String & Optionality
// PeerID <> String
extension Optional where Wrapped == PeerID {
static func ==(lhs: Optional<Wrapped>, rhs: Optional<String>) -> Bool { lhs?.id == rhs }
static func !=(lhs: Optional<Wrapped>, rhs: Optional<String>) -> Bool { lhs?.id != rhs }
}
// String <> PeerID
extension Optional where Wrapped == String {
static func ==(lhs: Optional<Wrapped>, rhs: Optional<PeerID>) -> Bool { lhs == rhs?.id }
static func !=(lhs: Optional<Wrapped>, rhs: Optional<PeerID>) -> Bool { lhs != rhs?.id }
}
+3 -3
View File
@@ -111,13 +111,13 @@ final class PrivateChatManager: ObservableObject {
// Route via MessageRouter to avoid handshakeRequired spam when session isn't established // Route via MessageRouter to avoid handshakeRequired spam when session isn't established
if let router = messageRouter { if let router = messageRouter {
SecureLogger.debug("PrivateChatManager: sending READ ack for \(message.id.prefix(8))… to \(senderPeerID.prefix(8))… via router", category: .session) SecureLogger.debug("PrivateChatManager: sending READ ack for \(message.id.prefix(8))… to \(senderPeerID.id.prefix(8))… via router", category: .session)
Task { @MainActor in Task { @MainActor in
router.sendReadReceipt(receipt, to: senderPeerID) router.sendReadReceipt(receipt, to: senderPeerID.id)
} }
} else { } else {
// Fallback: preserve previous behavior // Fallback: preserve previous behavior
meshService?.sendReadReceipt(receipt, to: senderPeerID) meshService?.sendReadReceipt(receipt, to: senderPeerID.id)
} }
} }
} }
+12 -12
View File
@@ -153,7 +153,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
@MainActor @MainActor
private func normalizedSenderKey(for message: BitchatMessage) -> String { private func normalizedSenderKey(for message: BitchatMessage) -> String {
if let spid = message.senderPeerID { if let spid = message.senderPeerID?.id {
if spid.hasPrefix("nostr:") || spid.hasPrefix("nostr_") { if spid.hasPrefix("nostr:") || spid.hasPrefix("nostr_") {
let bare: String = { let bare: String = {
if spid.hasPrefix("nostr:") { return String(spid.dropFirst(6)) } if spid.hasPrefix("nostr:") { return String(spid.dropFirst(6)) }
@@ -1967,7 +1967,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let gh = currentGeohash { if let gh = currentGeohash {
if var arr = geoTimelines[gh] { if var arr = geoTimelines[gh] {
arr.removeAll { msg in arr.removeAll { msg in
if let spid = msg.senderPeerID, spid.hasPrefix("nostr") { if let spid = msg.senderPeerID?.id, spid.hasPrefix("nostr") {
if let full = nostrKeyMapping[spid]?.lowercased() { return full == hex } if let full = nostrKeyMapping[spid]?.lowercased() { return full == hex }
} }
return false return false
@@ -1978,7 +1978,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
switch activeChannel { switch activeChannel {
case .location: case .location:
messages.removeAll { msg in messages.removeAll { msg in
if let spid = msg.senderPeerID, spid.hasPrefix("nostr") { if let spid = msg.senderPeerID?.id , spid.hasPrefix("nostr") {
if let full = nostrKeyMapping[spid]?.lowercased() { return full == hex } if let full = nostrKeyMapping[spid]?.lowercased() { return full == hex }
} }
return false return false
@@ -2676,7 +2676,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Store the Nostr pubkey if provided (for messages from unknown senders) // Store the Nostr pubkey if provided (for messages from unknown senders)
if let nostrPubkey = notification.userInfo?["nostrPubkey"] as? String, if let nostrPubkey = notification.userInfo?["nostrPubkey"] as? String,
let senderPeerID = message.senderPeerID { let senderPeerID = message.senderPeerID?.id {
// Store mapping for read receipts // Store mapping for read receipts
nostrKeyMapping[senderPeerID] = nostrPubkey nostrKeyMapping[senderPeerID] = nostrPubkey
} }
@@ -3296,7 +3296,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
func formatMessageAsText(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString { func formatMessageAsText(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
// Determine if this message was sent by self (mesh, geo, or DM) // Determine if this message was sent by self (mesh, geo, or DM)
let isSelf: Bool = { let isSelf: Bool = {
if let spid = message.senderPeerID { if let spid = message.senderPeerID?.id {
// In geohash channels, compare against our per-geohash nostr short ID // In geohash channels, compare against our per-geohash nostr short ID
if case .location(let ch) = activeChannel, spid.hasPrefix("nostr:") { if case .location(let ch) = activeChannel, spid.hasPrefix("nostr:") {
if let myGeo = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) { if let myGeo = try? NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash) {
@@ -3331,7 +3331,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let fontWeight: Font.Weight = isSelf ? .bold : .medium let fontWeight: Font.Weight = isSelf ? .bold : .medium
senderStyle.font = .bitchatSystem(size: 14, weight: fontWeight, design: .monospaced) senderStyle.font = .bitchatSystem(size: 14, weight: fontWeight, design: .monospaced)
// Make sender clickable: encode senderPeerID into a custom URL // Make sender clickable: encode senderPeerID into a custom URL
if let spid = message.senderPeerID, let url = URL(string: "bitchat://user/\(spid.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? spid)") { if let spid = message.senderPeerID?.id, let url = URL(string: "bitchat://user/\(spid.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? spid)") {
senderStyle.link = url senderStyle.link = url
} }
@@ -3862,7 +3862,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
@MainActor @MainActor
private func peerColor(for message: BitchatMessage, isDark: Bool) -> Color { private func peerColor(for message: BitchatMessage, isDark: Bool) -> Color {
if let spid = message.senderPeerID { if let spid = message.senderPeerID?.id {
if spid.hasPrefix("nostr:") || spid.hasPrefix("nostr_") { if spid.hasPrefix("nostr:") || spid.hasPrefix("nostr_") {
let bare: String = { let bare: String = {
if spid.hasPrefix("nostr:") { return String(spid.dropFirst(6)) } if spid.hasPrefix("nostr:") { return String(spid.dropFirst(6)) }
@@ -5654,7 +5654,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
/// Check if a message should be blocked based on sender /// Check if a message should be blocked based on sender
@MainActor @MainActor
private func isMessageBlocked(_ message: BitchatMessage) -> Bool { private func isMessageBlocked(_ message: BitchatMessage) -> Bool {
if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender) { if let peerID = message.senderPeerID?.id ?? getPeerIDForNickname(message.sender) {
// Check mesh/known peers first // Check mesh/known peers first
if isPeerBlocked(peerID) { return true } if isPeerBlocked(peerID) { return true }
// Check geohash (Nostr) blocks using mapping to full pubkey // Check geohash (Nostr) blocks using mapping to full pubkey
@@ -5694,7 +5694,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, senderPeerID: message.senderPeerID?.id,
mentions: message.mentions, mentions: message.mentions,
deliveryStatus: message.deliveryStatus deliveryStatus: message.deliveryStatus
) )
@@ -5802,7 +5802,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
@MainActor @MainActor
private func handlePrivateMessage(_ message: BitchatMessage) { private func handlePrivateMessage(_ message: BitchatMessage) {
SecureLogger.debug("📥 handlePrivateMessage called for message from \(message.sender)", category: .session) SecureLogger.debug("📥 handlePrivateMessage called for message from \(message.sender)", category: .session)
let senderPeerID = message.senderPeerID ?? getPeerIDForNickname(message.sender) let senderPeerID = message.senderPeerID?.id ?? getPeerIDForNickname(message.sender)
guard let peerID = senderPeerID else { guard let peerID = senderPeerID else {
SecureLogger.warning("⚠️ Could not get peer ID for sender \(message.sender)", category: .session) SecureLogger.warning("⚠️ Could not get peer ID for sender \(message.sender)", category: .session)
@@ -5912,7 +5912,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
readerNickname: nickname readerNickname: nickname
) )
let recipientID = message.senderPeerID ?? peerID let recipientID = message.senderPeerID?.id ?? peerID
Task { @MainActor in Task { @MainActor in
var originalTransport: String? = nil var originalTransport: String? = nil
@@ -5944,7 +5944,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if isMessageBlocked(finalMessage) { return } if isMessageBlocked(finalMessage) { return }
// Classify origin: geochat if senderPeerID starts with 'nostr:', else mesh (or system) // Classify origin: geochat if senderPeerID starts with 'nostr:', else mesh (or system)
let isGeo = finalMessage.senderPeerID?.hasPrefix("nostr:") == true let isGeo = finalMessage.senderPeerID?.isGeoChat == true
// Apply per-sender and per-content rate limits (drop if exceeded) // Apply per-sender and per-content rate limits (drop if exceeded)
if finalMessage.sender != "system" { if finalMessage.sender != "system" {
@@ -111,7 +111,7 @@ final class PublicChatE2ETests: XCTestCase {
originalSender: message.sender, originalSender: message.sender,
isPrivate: message.isPrivate, isPrivate: message.isPrivate,
recipientNickname: message.recipientNickname, recipientNickname: message.recipientNickname,
senderPeerID: message.senderPeerID, senderPeerID: message.senderPeerID?.id,
mentions: message.mentions mentions: message.mentions
) )
@@ -451,7 +451,7 @@ final class PublicChatE2ETests: XCTestCase {
originalSender: message.isRelay ? message.originalSender : message.sender, originalSender: message.isRelay ? message.originalSender : message.sender,
isPrivate: message.isPrivate, isPrivate: message.isPrivate,
recipientNickname: message.recipientNickname, recipientNickname: message.recipientNickname,
senderPeerID: message.senderPeerID, senderPeerID: message.senderPeerID?.id,
mentions: message.mentions mentions: message.mentions
) )
@@ -638,7 +638,7 @@ final class IntegrationTests: XCTestCase {
originalSender: message.isRelay ? message.originalSender : message.sender, originalSender: message.isRelay ? message.originalSender : message.sender,
isPrivate: message.isPrivate, isPrivate: message.isPrivate,
recipientNickname: message.recipientNickname, recipientNickname: message.recipientNickname,
senderPeerID: message.senderPeerID, senderPeerID: message.senderPeerID?.id,
mentions: message.mentions mentions: message.mentions
) )
+42
View File
@@ -290,6 +290,48 @@ final class PeerIDTests: XCTestCase {
XCTAssertEqual(sorted, [p1, p2]) XCTAssertEqual(sorted, [p1, p2])
} }
func test_equality() {
let string = "aaa"
let peerID = PeerID(str: string)
let badString = "bbb"
// PeerID == String
XCTAssertTrue(peerID == string)
XCTAssertTrue(peerID == Optional(string))
XCTAssertTrue(Optional(peerID) == string)
XCTAssertTrue(Optional(peerID) == Optional(string))
// PeerID != String
XCTAssertTrue(peerID != badString)
XCTAssertTrue(peerID != Optional(badString))
XCTAssertTrue(Optional(peerID) != badString)
XCTAssertTrue(Optional(peerID) != Optional(badString))
// String == PeerID
XCTAssertTrue(string == peerID)
XCTAssertTrue(Optional(string) == peerID)
XCTAssertTrue(string == Optional(peerID))
XCTAssertTrue(Optional(string) == Optional(peerID))
// String != PeerID
XCTAssertTrue(badString != peerID)
XCTAssertTrue(Optional(badString) != peerID)
XCTAssertTrue(badString != Optional(peerID))
XCTAssertTrue(Optional(badString) != Optional(peerID))
// Make sure the regular PeerID <> PeerID is not broken
XCTAssertTrue(peerID == PeerID(str: "aaa"))
XCTAssertTrue(peerID == Optional(PeerID(str: "aaa")))
XCTAssertTrue(PeerID(str: "aaa") == peerID)
XCTAssertTrue(Optional(PeerID(str: "aaa")) == Optional(peerID))
XCTAssertTrue(peerID != PeerID(str: "bbb"))
XCTAssertTrue(peerID != Optional(PeerID(str: "bbb")))
XCTAssertTrue(PeerID(str: "bbb") != peerID)
XCTAssertTrue(Optional(PeerID(str: "bbb")) != Optional(peerID))
}
// MARK: - Computed properties // MARK: - Computed properties
func test_isEmpty_true_and_false() { func test_isEmpty_true_and_false() {