damus

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

NostrScript.swift (10831B)


      1 //
      2 //  NostrScript.swift
      3 //  damus
      4 //
      5 //  Created by William Casarin on 2023-06-02.
      6 //
      7 
      8 import Foundation
      9 
     10 enum NostrScriptLoadErr {
     11     case parse
     12     case module_init
     13 }
     14 
     15 enum NostrScriptRunResult {
     16     case runtime_err([String])
     17     case suspend
     18     case finished(Int)
     19     
     20     var exited: Bool {
     21         switch self {
     22         case .runtime_err:
     23             return true
     24         case .suspend:
     25             return false
     26         case .finished:
     27             return true
     28         }
     29     }
     30     
     31     var is_suspended: Bool {
     32         if case .suspend = self {
     33             return true
     34         }
     35         return false
     36     }
     37 }
     38 
     39 enum NostrScriptLoadResult {
     40     case err(NostrScriptLoadErr)
     41     case loaded(wasm_interp)
     42 }
     43 
     44 enum NostrScriptError: Error {
     45     case not_loaded
     46 }
     47 
     48 class NostrScript {
     49     private var interp: wasm_interp
     50     private var parser: wasm_parser
     51     var waiting_on: NScriptWaiting?
     52     var loaded: Bool
     53     var data: [UInt8]
     54     
     55     private(set) var runstate: NostrScriptRunResult?
     56     private(set) var pool: RelayPool
     57     private(set) var event: NostrResponse?
     58     
     59     init(pool: RelayPool, data: [UInt8]) {
     60         self.interp = wasm_interp()
     61         self.parser = wasm_parser()
     62         self.pool = pool
     63         self.event = nil
     64         self.runstate = nil
     65         self.loaded = false
     66         self.data = data
     67     }
     68     
     69     deinit {
     70         wasm_parser_free(&self.parser)
     71         wasm_interp_free(&self.interp)
     72     }
     73     
     74     func is_suspended(on: NScriptWaiting) -> Bool {
     75         return self.waiting_on == on
     76     }
     77     
     78     func can_resume(with: NScriptResumeWith) -> Bool {
     79         guard let waiting_on else {
     80             return false
     81         }
     82         switch waiting_on {
     83         case .event(let subid):
     84             switch with {
     85             case .event(let resp):
     86                 return resp.subid == subid
     87             }
     88         }
     89     }
     90     
     91     func imports() -> [String] {
     92         guard self.loaded,
     93               was_section_parsed(interp.module, section_import) > 0,
     94               let module = maybe_pointee(interp.module)
     95         else {
     96             return []
     97         }
     98         
     99         var imports = [String]()
    100         
    101         var i = 0
    102         while i < module.import_section.num_imports {
    103             let imp = module.import_section.imports[i]
    104             
    105             imports.append(String(cString: imp.name))
    106             
    107             i += 1
    108         }
    109         
    110         return imports
    111     }
    112     
    113     func load() -> NostrScriptLoadErr? {
    114         guard !loaded else {
    115             return nil
    116         }
    117         switch nscript_load(&parser, &interp, &self.data, UInt(data.count))  {
    118         case NSCRIPT_LOADED:
    119             print("load num_exports \(interp.module.pointee.export_section.num_exports)")
    120             interp.context = Unmanaged.passUnretained(self).toOpaque()
    121             self.loaded = true
    122             return nil
    123         case NSCRIPT_INIT_ERR:
    124             return .module_init
    125         case NSCRIPT_PARSE_ERR:
    126             return .parse
    127         default:
    128             return .parse
    129         }
    130     }
    131     
    132     func resume(with: NScriptResumeWith) -> NostrScriptRunResult? {
    133         guard let runstate, runstate.is_suspended, can_resume(with: with) else {
    134             return nil
    135         }
    136         
    137         switch with {
    138         case .event(let resp):
    139             load_data(resp: resp)
    140         }
    141                 
    142         let st = nscript_run(interp: &interp, resuming: true)
    143         self.runstate = st
    144         self.event = nil
    145         return st
    146     }
    147     
    148     private func load_data(resp: NostrResponse) {
    149         self.event = resp
    150     }
    151     
    152     func run() -> NostrScriptRunResult {
    153         if let runstate {
    154             return runstate
    155         }
    156 
    157         let st = nscript_run(interp: &interp, resuming: false)
    158         self.runstate = st
    159         return st
    160     }
    161 }
    162 
    163 fileprivate func interp_nostrscript(interp: UnsafeMutablePointer<wasm_interp>?) -> NostrScript? {
    164     guard let interp = interp?.pointee else {
    165         return nil
    166     }
    167     
    168     return Unmanaged<NostrScript>.fromOpaque(interp.context).takeUnretainedValue()
    169 }
    170 
    171 fileprivate func asm_str_byteptr(cstr: UnsafePointer<UInt8>, len: Int32) -> String? {
    172     let u16 = cstr.withMemoryRebound(to: UInt16.self, capacity: Int(len)) { p in p }
    173     return asm_str(cstr: u16, len: len)
    174 }
    175 
    176 fileprivate func asm_str(cstr: UnsafePointer<UInt16>, len: Int32) -> String? {
    177     return String(utf16CodeUnits: cstr, count: Int(len))
    178 }
    179 
    180 enum NScriptCommand: Int {
    181     case pool_send = 1
    182     case add_relay = 2
    183     case event_await = 3
    184     case event_get_type = 4
    185     case event_get_note = 5
    186     case note_get_kind = 6
    187     case note_get_content = 7
    188     case note_get_content_length = 8
    189 }
    190 
    191 enum NScriptEventType: Int {
    192     case ok = 1
    193     case note = 2
    194     case notice = 3
    195     case eose = 4
    196     case auth = 5
    197 
    198     init(resp: NostrResponse) {
    199         switch resp {
    200         case .event:
    201             self = .note
    202         case .notice:
    203             self = .notice
    204         case .eose:
    205             self = .eose
    206         case .ok:
    207             self = .ok
    208         case .auth:
    209             self = .auth
    210         }
    211     }
    212 }
    213 
    214 enum NScriptWaiting: Equatable {
    215     case event(String)
    216     
    217     var subid: String {
    218         switch self {
    219         case .event(let subid):
    220             return subid
    221         }
    222     }
    223 }
    224 
    225 enum NScriptResumeWith {
    226     case event(NostrResponse)
    227 }
    228 
    229 enum NScriptCmdResult {
    230     case suspend(NScriptWaiting)
    231     case ok
    232     case fatal
    233 }
    234 
    235 @_cdecl("nscript_nostr_cmd")
    236 public func nscript_nostr_cmd(interp: UnsafeMutablePointer<wasm_interp>?, cmd: Int32, value: UnsafePointer<UInt8>, len: Int32) -> Int32 {
    237     guard let script = interp_nostrscript(interp: interp),
    238           let cmd = NScriptCommand(rawValue: Int(cmd)) else {
    239         return 0
    240     }
    241     
    242     print("nostr_cmd \(cmd)")
    243     
    244     switch cmd {
    245     case .pool_send:
    246         guard let req = asm_str_byteptr(cstr: value, len: len) else { return 0 }
    247         let res = nscript_pool_send(script: script, req: req)
    248         stack_push_i32(interp, 0);
    249         return res;
    250         
    251     case .add_relay:
    252         guard let relay = asm_str_byteptr(cstr: value, len: len) else { return 0 }
    253         let ok = nscript_add_relay(script: script, relay: relay)
    254         stack_push_i32(interp, ok ? 1 : 0)
    255         return 1;
    256         
    257     case .event_await:
    258         guard let subid = asm_str_byteptr(cstr: value, len: len) else { return 0 }
    259         nscript_event_await(script: script, subid: subid)
    260         let ev_handle: Int32 = 1
    261         stack_push_i32(interp, ev_handle);
    262         return BUILTIN_SUSPEND
    263         
    264     case .event_get_type:
    265         guard let event = script.event else {
    266             stack_push_i32(interp, 0);
    267             return 1
    268         }
    269         
    270         let type = NScriptEventType(resp: event)
    271         stack_push_i32(interp, Int32(type.rawValue));
    272         return 1
    273         
    274     case .event_get_note:
    275         guard let event = script.event, case .event = event
    276         else { stack_push_i32(interp, 0); return 1 }
    277         
    278         let note_handle: Int32 = 1
    279         stack_push_i32(interp, note_handle)
    280         return 1
    281         
    282     case .note_get_kind:
    283         guard let event = script.event, case .event(_, let note) = event
    284         else {
    285             stack_push_i32(interp, 0);
    286             return 1
    287             
    288         }
    289         
    290         stack_push_i32(interp, Int32(note.kind))
    291         return 1
    292         
    293     case .note_get_content:
    294         guard let event = script.event, case .event(_, let note) = event
    295         else { stack_push_i32(interp, 0); return 1 }
    296         
    297         stack_push_i32(interp, Int32(note.kind))
    298         return 1
    299     
    300     case .note_get_content_length:
    301         guard let event = script.event, case .event(_, let note) = event
    302         else { stack_push_i32(interp, 0); return 1 }
    303         
    304         stack_push_i32(interp, Int32(note.content.utf8.count))
    305         return 1
    306     }
    307     
    308 }
    309 
    310 func nscript_add_relay(script: NostrScript, relay: String) -> Bool {
    311     guard let url = RelayURL(relay) else { return false }
    312     let desc = RelayDescriptor(url: url, info: .rw, variant: .ephemeral)
    313     return (try? script.pool.add_relay(desc)) != nil
    314 }
    315 
    316 
    317 @_cdecl("nscript_set_bool")
    318 public func nscript_set_bool(interp: UnsafeMutablePointer<wasm_interp>?, setting: UnsafePointer<UInt16>, setting_len: Int32, val: Int32) -> Int32 {
    319     
    320     guard let setting = asm_str(cstr: setting, len: setting_len),
    321           UserSettingsStore.bool_options.contains(setting)
    322     else {
    323         stack_push_i32(interp, 0);
    324         return 1;
    325     }
    326     
    327     let key = pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: setting)
    328     let b = val > 0 ? true : false
    329     print("nscript setting bool setting \(setting) to \(b)")
    330     UserDefaults.standard.set(b, forKey: key)
    331     
    332     stack_push_i32(interp, 1);
    333     return 1;
    334 }
    335 
    336 @_cdecl("nscript_pool_send_to")
    337 public func nscript_pool_send_to(interp: UnsafeMutablePointer<wasm_interp>?, preq: UnsafePointer<UInt16>, req_len: Int32, to: UnsafePointer<UInt16>, to_len: Int32) -> Int32 {
    338 
    339     guard let script = interp_nostrscript(interp: interp),
    340           let req_str = asm_str(cstr: preq, len: req_len),
    341           let to = asm_str(cstr: to, len: to_len),
    342           let to_relay_url = RelayURL(to)
    343     else {
    344         return 0
    345     }
    346 
    347     DispatchQueue.main.async {
    348         script.pool.send_raw(.custom(req_str), to: [to_relay_url], skip_ephemeral: false)
    349     }
    350 
    351     return 1;
    352 }
    353 
    354 func nscript_pool_send(script: NostrScript, req req_str: String) -> Int32 {
    355     //script.test("pool_send: '\(req_str)'")
    356     
    357     DispatchQueue.main.sync {
    358         script.pool.send_raw(.custom(req_str), skip_ephemeral: false)
    359     }
    360     
    361     return 1;
    362 }
    363 
    364 func nscript_event_await(script: NostrScript, subid: String)  {
    365     script.waiting_on = .event(subid)
    366 }
    367 
    368 func nscript_get_error_backtrace(errors: inout errors) -> [String] {
    369     var xs = [String]()
    370     var errs = cursor()
    371     var err = error()
    372 
    373     copy_cursor(&errors.cur, &errs)
    374     errs.p = errs.start;
    375 
    376     while (errs.p < errors.cur.p) {
    377         if (cursor_pull_error(&errs, &err) == 0) {
    378             return xs
    379         }
    380         
    381         xs.append(String(cString: err.msg))
    382     }
    383     
    384     return xs
    385 }
    386 
    387 func nscript_run(interp: inout wasm_interp, resuming: Bool) -> NostrScriptRunResult {
    388     var res: Int32 = 0
    389     var retval: Int32 = 0
    390         
    391     if (resuming) {
    392         print("resuming nostrscript");
    393         res = interp_wasm_module_resume(&interp, &retval);
    394     } else {
    395         res = interp_wasm_module(&interp, &retval);
    396     }
    397     
    398     if res == 0 {
    399         print_callstack(&interp);
    400         print_error_backtrace(&interp.errors);
    401         let backtrace = nscript_get_error_backtrace(errors: &interp.errors)
    402         return .runtime_err(backtrace)
    403     }
    404     
    405     if res == BUILTIN_SUSPEND {
    406         return .suspend
    407     }
    408 
    409     //print_stack(&interp.stack);
    410     wasm_interp_free(&interp);
    411 
    412     return .finished(Int(retval))
    413 }
    414