NdbBlock.swift (11548B)
1 // 2 // NdbBlock.swift 3 // damus 4 // 5 // Created by William Casarin on 2024-01-25. 6 // 7 8 import Foundation 9 10 enum NdbBlockType: UInt32 { 11 case hashtag = 1 12 case text = 2 13 case mention_index = 3 14 case mention_bech32 = 4 15 case url = 5 16 case invoice = 6 17 } 18 19 extension ndb_mention_bech32_block { 20 var bech32_type: NdbBech32Type? { 21 NdbBech32Type(rawValue: self.bech32.type.rawValue) 22 } 23 } 24 25 enum NdbBech32Type: UInt32 { 26 case note = 1 27 case npub = 2 28 case nprofile = 3 29 case nevent = 4 30 case nrelay = 5 31 case naddr = 6 32 case nsec = 7 33 34 var is_notelike: Bool { 35 return self == .note || self == .nevent 36 } 37 } 38 39 extension ndb_invoice_block { 40 func as_invoice() -> Invoice? { 41 let b11 = self.invoice 42 let invstr = self.invstr.as_str() 43 44 guard let description = convert_invoice_description(b11: b11) else { 45 return nil 46 } 47 48 let amount: Amount = b11.amount == 0 ? .any : .specific(Int64(b11.amount)) 49 50 return Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, created_at: b11.timestamp) 51 } 52 } 53 54 enum NdbBlock: ~Copyable { 55 case text(ndb_str_block) 56 case mention(ndb_mention_bech32_block) 57 case hashtag(ndb_str_block) 58 case url(ndb_str_block) 59 case invoice(ndb_invoice_block) 60 case mention_index(UInt32) 61 62 init?(_ ptr: ndb_block_ptr) { 63 guard let type = NdbBlockType(rawValue: ndb_get_block_type(ptr.ptr).rawValue) else { 64 return nil 65 } 66 switch type { 67 case .hashtag: self = .hashtag(ptr.block.str) 68 case .text: self = .text(ptr.block.str) 69 case .invoice: self = .invoice(ptr.block.invoice) 70 case .url: self = .url(ptr.block.str) 71 case .mention_bech32: self = .mention(ptr.block.mention_bech32) 72 case .mention_index: self = .mention_index(ptr.block.mention_index) 73 } 74 } 75 76 var is_previewable: Bool { 77 switch self { 78 case .mention(let m): 79 switch m.bech32_type { 80 case .note, .nevent: return true 81 default: return false 82 } 83 case .invoice: 84 return true 85 case .url: 86 return true 87 default: 88 return false 89 } 90 } 91 92 static func convertToStringCopy(from block: str_block_t) -> String? { 93 guard let cString = block.str else { 94 return nil 95 } 96 // Copy byte-by-byte from the pointer into a new buffer 97 let byteBuffer = UnsafeBufferPointer(start: cString, count: Int(block.len)).map { UInt8(bitPattern: $0) } 98 99 // Create an owned Swift String from the buffer we created 100 return String(bytes: byteBuffer, encoding: .utf8) 101 } 102 } 103 104 /// Represents a group of blocks 105 struct NdbBlockGroup: ~Copyable { 106 /// The block offsets 107 fileprivate let metadata: MaybeTxn<BlocksMetadata> 108 /// The raw text content of the note 109 fileprivate let rawTextContent: String 110 var words: Int { 111 return metadata.borrow { $0.words } 112 } 113 114 /// Gets the parsed blocks from a specific note. 115 /// 116 /// This function will: 117 /// - fetch blocks information from NostrDB if possible _and_ available, or 118 /// - parse blocks on-demand. 119 static func from(event: NdbNote, using ndb: Ndb, and keypair: Keypair) throws(NdbBlocksError) -> Self { 120 if event.is_content_encrypted() { 121 return try parse(event: event, keypair: keypair) 122 } 123 else if event.known_kind == .highlight { 124 return try parse(event: event, keypair: keypair) 125 } 126 else { 127 guard let offsets = event.block_offsets(ndb: ndb) else { 128 return try parse(event: event, keypair: keypair) 129 } 130 return .init(metadata: .txn(offsets), rawTextContent: event.content) 131 } 132 } 133 134 /// Parses the note contents on-demand from a specific note. 135 /// 136 /// Prioritize using `from(event: NdbNote, using ndb: Ndb, and keypair: Keypair)` when possible. 137 static func parse(event: NdbNote, keypair: Keypair) throws(NdbBlocksError) -> Self { 138 guard let content = event.maybe_get_content(keypair) else { throw NdbBlocksError.decryptionError } 139 guard let metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError } 140 return self.init( 141 metadata: .pure(metadata), 142 rawTextContent: content 143 ) 144 } 145 146 /// Parses the note contents on-demand from a specific text. 147 static func parse(content: String) throws(NdbBlocksError) -> Self { 148 guard let metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError } 149 return self.init( 150 metadata: .pure(metadata), 151 rawTextContent: content 152 ) 153 } 154 } 155 156 enum MaybeTxn<T: ~Copyable>: ~Copyable { 157 case pure(T) 158 case txn(SafeNdbTxn<T>) 159 160 func borrow<Y>(_ borrowFunction: (borrowing T) throws -> Y) rethrows -> Y { 161 switch self { 162 case .pure(let item): 163 return try borrowFunction(item) 164 case .txn(let txn): 165 return try borrowFunction(txn.val) 166 } 167 } 168 } 169 170 171 // MARK: - Helper structs 172 173 extension NdbBlockGroup { 174 /// Wrapper for the `ndb_blocks` C struct 175 /// 176 /// This does not store the actual block contents, only the offsets on the content string and block metadata. 177 /// 178 /// **Implementation note:** This would be better as `~Copyable`, but `NdbTxn` does not support `~Copyable` yet. 179 struct BlocksMetadata: ~Copyable { 180 private let blocks_ptr: ndb_blocks_ptr 181 private let buffer: UnsafeMutableRawPointer? 182 183 init(ptr: OpaquePointer?, buffer: UnsafeMutableRawPointer? = nil) { 184 self.blocks_ptr = ndb_blocks_ptr(ptr: ptr) 185 self.buffer = buffer 186 } 187 188 var words: Int { 189 Int(ndb_blocks_word_count(blocks_ptr.ptr)) 190 } 191 192 /// Gets the opaque pointer 193 /// 194 /// **Implementation note:** This is marked `fileprivate` because we want to minimize the exposure of raw pointers to Swift code outside these wrapper structs. 195 fileprivate func as_ptr() -> OpaquePointer? { 196 return self.blocks_ptr.ptr 197 } 198 199 /// Parses text content and returns the parsed block metadata if successful 200 /// 201 /// **Implementation notes:** This is `fileprivate` because it makes no sense for outside Swift code to use this directly. Use `NdbBlockGroup` instead. 202 fileprivate static func parseContent(content: String) -> Self? { 203 // Allocate scratch buffer with enough space 204 guard let buffer = malloc(MAX_NOTE_SIZE) else { 205 return nil 206 } 207 208 var blocks: OpaquePointer? = nil 209 210 // Call the C parsing function and check its success status 211 let success = content.withCString { contentPtr -> Bool in 212 let contentLen = content.utf8.count 213 return ndb_parse_content( 214 buffer.assumingMemoryBound(to: UInt8.self), 215 Int32(MAX_NOTE_SIZE), 216 contentPtr, 217 Int32(contentLen), 218 &blocks 219 ) == 1 220 } 221 222 if !success || blocks == nil { 223 // Something failed 224 free(buffer) 225 return nil 226 } 227 228 // TODO: We should set the owned flag as in the C code. 229 // However, There does not seem to be a way to set this from Swift code. The code shown below does not work. 230 // blocks!.pointee.flags |= NDB_BLOCK_FLAG_OWNED 231 // But perhaps this is not necessary because `NdbBlockGroup` is non-copyable 232 233 return BlocksMetadata(ptr: blocks, buffer: buffer) 234 } 235 236 deinit { 237 if let buffer { 238 free(buffer) 239 } 240 } 241 } 242 243 /// Models specific errors that may happen when parsing or constructing an `NdbBlocks` object 244 enum NdbBlocksError: Error { 245 case parseError 246 case decryptionError 247 } 248 } 249 250 251 // MARK: - Enumeration support 252 253 extension NdbBlockGroup { 254 typealias NdbBlockList = NonCopyableLinkedList<NdbBlock> 255 256 /// Borrows all blocks in the group one by one and runs a function defined by the caller. 257 /// 258 /// **Implementation note:** 259 /// This is done as a function instead of using `Sequence` and `Iterator` protocols because it is currently not possible to conform to both `Sequence` and `~Copyable` at the same time, as Sequence requires elements to be `Copyable` 260 /// 261 /// - Parameter borrowingFunction: The function to be run on each iteration. Takes in two parameters: The index of the item in the list (zero-indexed), and the block itself. 262 /// - Returns: The `Y` value returned by the provided function, when such function returns `.loopReturn(Y)` 263 @discardableResult 264 func forEachBlock<Y>(_ borrowingFunction: ((Int, borrowing NdbBlock) throws -> NdbBlockList.LoopCommand<Y>)) rethrows -> Y? { 265 return try withList({ try $0.forEachItem(borrowingFunction) }) 266 } 267 268 /// Borrows all blocks in the group one by one and runs a function defined by the caller, in reverse order 269 /// 270 /// **Implementation note:** 271 /// This is done as a function instead of using `Sequence` and `Iterator` protocols because it is currently not possible to conform to both `Sequence` and `~Copyable` at the same time, as Sequence requires elements to be `Copyable` 272 /// 273 /// - Parameter borrowingFunction: The function to be run on each iteration. Takes in two parameters: The index of the item in the list (zero-indexed), and the block itself. 274 /// - Returns: The `Y` value returned by the provided function, when such function returns `.loopReturn(Y)` 275 @discardableResult 276 func forEachBlockReversed<Y>(_ borrowingFunction: ((Int, borrowing NdbBlock) throws -> NdbBlockList.LoopCommand<Y>)) rethrows -> Y? { 277 return try withList({ try $0.forEachItemReversed(borrowingFunction) }) 278 } 279 280 /// Iterates over each item of the list, updating a final value, and returns the final result at the end. 281 func reduce<Y>(initialResult: Y, _ borrowingFunction: ((_ index: Int, _ partialResult: Y, _ item: borrowing NdbBlock) throws -> NdbBlockList.LoopCommand<Y>)) rethrows -> Y? { 282 return try withList({ try $0.reduce(initialResult: initialResult, borrowingFunction) }) 283 } 284 285 /// Borrows the block list for processing 286 func withList<Y>(_ borrowingFunction: (borrowing NdbBlockList) throws -> Y) rethrows -> Y { 287 var linkedList: NdbBlockList = .init() 288 289 return try self.rawTextContent.withCString { cptr in 290 var iter = ndb_block_iterator(content: cptr, blocks: nil, block: ndb_block(), p: nil) 291 292 // Start the iteration 293 return try self.metadata.borrow { value in 294 ndb_blocks_iterate_start(cptr, value.as_ptr(), &iter) 295 296 // Collect blocks into array 297 outerLoop: while let ptr = ndb_blocks_iterate_next(&iter), 298 let block = NdbBlock(ndb_block_ptr(ptr: ptr)) { 299 linkedList.add(item: block) 300 } 301 302 return try borrowingFunction(linkedList) 303 } 304 } 305 } 306 }