mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
PeerID 4/n: BitchatMessage.senderPeerID + String equality (#739)
This commit is contained in:
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user