// // BitchatMessage.swift // bitchat // // This is free and unencumbered software released into the public domain. // For more information, see // import class Foundation.DateFormatter import struct Foundation.AttributedString import struct Foundation.Data import struct Foundation.Date import struct Foundation.TimeInterval import struct Foundation.UUID /// Represents a user-visible message in the BitChat system. /// Handles both broadcast messages and private encrypted messages, /// with support for mentions, replies, and delivery tracking. /// - Note: This is the primary data model for chat messages public final class BitchatMessage: Codable { public let id: String public let sender: String public let content: String public let timestamp: Date public let isRelay: Bool public let originalSender: String? public let isPrivate: Bool public let recipientNickname: String? public let senderPeerID: PeerID? public let mentions: [String]? // Array of mentioned nicknames public var deliveryStatus: DeliveryStatus? // Delivery tracking /// True when this message reached us across a mesh bridge (signed by its /// author for an internet rendezvous) rather than over local radio. public let isBridged: Bool // Cached formatted text (not included in Codable) private var _cachedFormattedText: [String: AttributedString] = [:] public func getCachedFormattedText(isDark: Bool, isSelf: Bool, variant: String = "") -> AttributedString? { return _cachedFormattedText["\(variant)\(isDark)-\(isSelf)"] } public func setCachedFormattedText(_ text: AttributedString, isDark: Bool, isSelf: Bool, variant: String = "") { _cachedFormattedText["\(variant)\(isDark)-\(isSelf)"] = text } // Codable implementation enum CodingKeys: String, CodingKey { case id, sender, content, timestamp, isRelay, originalSender case isPrivate, recipientNickname, senderPeerID, mentions, deliveryStatus case isBridged } public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) sender = try container.decode(String.self, forKey: .sender) content = try container.decode(String.self, forKey: .content) timestamp = try container.decode(Date.self, forKey: .timestamp) isRelay = try container.decode(Bool.self, forKey: .isRelay) originalSender = try container.decodeIfPresent(String.self, forKey: .originalSender) isPrivate = try container.decode(Bool.self, forKey: .isPrivate) recipientNickname = try container.decodeIfPresent(String.self, forKey: .recipientNickname) senderPeerID = try container.decodeIfPresent(PeerID.self, forKey: .senderPeerID) mentions = try container.decodeIfPresent([String].self, forKey: .mentions) deliveryStatus = try container.decodeIfPresent(DeliveryStatus.self, forKey: .deliveryStatus) // Absent in archives written before bridging existed. isBridged = try container.decodeIfPresent(Bool.self, forKey: .isBridged) ?? false } public init( id: String? = nil, sender: String, content: String, timestamp: Date, isRelay: Bool, originalSender: String? = nil, isPrivate: Bool = false, recipientNickname: String? = nil, senderPeerID: PeerID? = nil, mentions: [String]? = nil, deliveryStatus: DeliveryStatus? = nil, isBridged: Bool = false ) { self.id = id ?? UUID().uuidString self.sender = sender self.content = content self.timestamp = timestamp self.isRelay = isRelay self.originalSender = originalSender self.isPrivate = isPrivate self.recipientNickname = recipientNickname self.senderPeerID = senderPeerID self.mentions = mentions self.deliveryStatus = deliveryStatus ?? (isPrivate ? .sending : nil) self.isBridged = isBridged } } // MARK: - Equatable Conformance extension BitchatMessage: Equatable { public static func == (lhs: BitchatMessage, rhs: BitchatMessage) -> Bool { return lhs.id == rhs.id && lhs.sender == rhs.sender && lhs.content == rhs.content && lhs.timestamp == rhs.timestamp && lhs.isRelay == rhs.isRelay && lhs.originalSender == rhs.originalSender && lhs.isPrivate == rhs.isPrivate && lhs.recipientNickname == rhs.recipientNickname && lhs.senderPeerID == rhs.senderPeerID && lhs.mentions == rhs.mentions && lhs.deliveryStatus == rhs.deliveryStatus && lhs.isBridged == rhs.isBridged } } // MARK: - Binary encoding extension BitchatMessage { func toBinaryPayload() -> Data? { var data = Data() // Message format: // - Flags: 1 byte (bit 0: isRelay, bit 1: isPrivate, bit 2: hasOriginalSender, bit 3: hasRecipientNickname, bit 4: hasSenderPeerID, bit 5: hasMentions) // - Timestamp: 8 bytes (seconds since epoch) // - ID length: 1 byte // - ID: variable // - Sender length: 1 byte // - Sender: variable // - Content length: 2 bytes // - Content: variable // Optional fields based on flags: // - Original sender length + data // - Recipient nickname length + data // - Sender peer ID length + data // - Mentions array var flags: UInt8 = 0 if isRelay { flags |= 0x01 } if isPrivate { flags |= 0x02 } if originalSender != nil { flags |= 0x04 } if recipientNickname != nil { flags |= 0x08 } if senderPeerID != nil { flags |= 0x10 } if mentions != nil && !mentions!.isEmpty { flags |= 0x20 } if isBridged { flags |= 0x40 } data.append(flags) // Timestamp (in milliseconds) let timestampMillis = UInt64(timestamp.timeIntervalSince1970 * 1000) // Encode as 8 bytes, big-endian for i in (0..<8).reversed() { data.append(UInt8((timestampMillis >> (i * 8)) & 0xFF)) } // ID if let idData = id.data(using: .utf8) { data.append(UInt8(min(idData.count, 255))) data.append(idData.prefix(255)) } else { data.append(0) } // Sender if let senderData = sender.data(using: .utf8) { data.append(UInt8(min(senderData.count, 255))) data.append(senderData.prefix(255)) } else { data.append(0) } // Content if let contentData = content.data(using: .utf8) { let length = UInt16(min(contentData.count, 65535)) // Encode length as 2 bytes, big-endian data.append(UInt8((length >> 8) & 0xFF)) data.append(UInt8(length & 0xFF)) data.append(contentData.prefix(Int(length))) } else { data.append(contentsOf: [0, 0]) } // Optional fields if let originalSender = originalSender, let origData = originalSender.data(using: .utf8) { data.append(UInt8(min(origData.count, 255))) data.append(origData.prefix(255)) } if let recipientNickname = recipientNickname, let recipData = recipientNickname.data(using: .utf8) { data.append(UInt8(min(recipData.count, 255))) data.append(recipData.prefix(255)) } if let peerData = senderPeerID?.id.data(using: .utf8) { data.append(UInt8(min(peerData.count, 255))) data.append(peerData.prefix(255)) } // Mentions array if let mentions = mentions { data.append(UInt8(min(mentions.count, 255))) // Number of mentions for mention in mentions.prefix(255) { if let mentionData = mention.data(using: .utf8) { data.append(UInt8(min(mentionData.count, 255))) data.append(mentionData.prefix(255)) } else { data.append(0) } } } return data } convenience init?(_ data: Data) { // Create an immutable copy to prevent threading issues let dataCopy = Data(data) guard dataCopy.count >= 13 else { return nil } var offset = 0 // Flags guard offset < dataCopy.count else { return nil } let flags = dataCopy[offset]; offset += 1 let isRelay = (flags & 0x01) != 0 let isPrivate = (flags & 0x02) != 0 let hasOriginalSender = (flags & 0x04) != 0 let hasRecipientNickname = (flags & 0x08) != 0 let hasSenderPeerID = (flags & 0x10) != 0 let hasMentions = (flags & 0x20) != 0 let isBridged = (flags & 0x40) != 0 // Timestamp guard offset + 8 <= dataCopy.count else { return nil } let timestampData = dataCopy[offset.. 0 { mentions = [] for _ in 0..