damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }