1
0
mirror of git://jb55.com/damus synced 2024-09-29 00:10:43 +00:00
damus/nostrscript/NostrScript.swift
Daniel D’Aquino e951370a76 Fix relay URL trailing slash issues
This commit tries to replace all usage of `String` to represent relay
URLs and use `RelayURL` which automatically converts strings to a
canonical relay URL format that is more reliable and avoids issues related to
trailing slashes.

Test 1: Main issue fix
-----------------------

PASS

Device: iPhone 15 Simulator
iOS: 17.4
Damus: This commit
Steps:
1. Delete all connected relays
2. Add `wss://relay.damus.io/` (with the trailing slash) to the relay list
3. Try to post. Post should succeed. PASS
4. Try removing this newly added relay. Relay should be removed successfully. PASS

Test 2: Persistent relay list after upgrade
--------------------------------------------

PASS

Device: iPhone 15 Simulator
iOS: 17.4
Damus: 1.8 (1) `247f313b` + This commit
Steps:
1. Downgrade to old version
2. Add some relays to the list, some without a trailing slash, some with
3. Upgrade to this commit
4. All relays added in step 2 should still be there, and ones with a trailing slash should have been corrected to remove the trailing slash

Test 3: Miscellaneous regression tests
--------------------------------------

Device: iPhone 15 Simulator
iOS: 17.4
Damus: This commit
Coverage:
1. Posting works
2. Search works
3. Relay connection status works
4. Adding relays work
5. Removing relays work
6. Adding relay with trailing slashes works (it fixes itself to remove the trailing slash)
7. Adding relays with different paths works (e.g. wss://yabu.me/v1 and wss://yabu.me/v2)
8. Adding duplicate relay (but with trailing slash) gets rejected as expected
9. Relay details page works. All items on that view loads correctly
10. Relay logs work
11. Getting follower counts and seeing follow lists on profiles still work
12. Relay list changes persist after app restart
13. Notifications view still work
14. Copying the user's pubkey and profile link works
15. Share note + copy link button still works
16. Connecting NWC wallet works
17. One-tap zaps work
18. Onboarding works
19. Unit tests all passing

Closes: https://github.com/damus-io/damus/issues/2072
Changelog-Fixed: Fix bug that would cause connection issues with relays defined with a trailing slash URL, and an inability to delete them.
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-03-25 09:24:17 +01:00

415 lines
11 KiB
Swift

//
// NostrScript.swift
// damus
//
// Created by William Casarin on 2023-06-02.
//
import Foundation
enum NostrScriptLoadErr {
case parse
case module_init
}
enum NostrScriptRunResult {
case runtime_err([String])
case suspend
case finished(Int)
var exited: Bool {
switch self {
case .runtime_err:
return true
case .suspend:
return false
case .finished:
return true
}
}
var is_suspended: Bool {
if case .suspend = self {
return true
}
return false
}
}
enum NostrScriptLoadResult {
case err(NostrScriptLoadErr)
case loaded(wasm_interp)
}
enum NostrScriptError: Error {
case not_loaded
}
class NostrScript {
private var interp: wasm_interp
private var parser: wasm_parser
var waiting_on: NScriptWaiting?
var loaded: Bool
var data: [UInt8]
private(set) var runstate: NostrScriptRunResult?
private(set) var pool: RelayPool
private(set) var event: NostrResponse?
init(pool: RelayPool, data: [UInt8]) {
self.interp = wasm_interp()
self.parser = wasm_parser()
self.pool = pool
self.event = nil
self.runstate = nil
self.loaded = false
self.data = data
}
deinit {
wasm_parser_free(&self.parser)
wasm_interp_free(&self.interp)
}
func is_suspended(on: NScriptWaiting) -> Bool {
return self.waiting_on == on
}
func can_resume(with: NScriptResumeWith) -> Bool {
guard let waiting_on else {
return false
}
switch waiting_on {
case .event(let subid):
switch with {
case .event(let resp):
return resp.subid == subid
}
}
}
func imports() -> [String] {
guard self.loaded,
was_section_parsed(interp.module, section_import) > 0,
let module = maybe_pointee(interp.module)
else {
return []
}
var imports = [String]()
var i = 0
while i < module.import_section.num_imports {
let imp = module.import_section.imports[i]
imports.append(String(cString: imp.name))
i += 1
}
return imports
}
func load() -> NostrScriptLoadErr? {
guard !loaded else {
return nil
}
switch nscript_load(&parser, &interp, &self.data, UInt(data.count)) {
case NSCRIPT_LOADED:
print("load num_exports \(interp.module.pointee.export_section.num_exports)")
interp.context = Unmanaged.passUnretained(self).toOpaque()
self.loaded = true
return nil
case NSCRIPT_INIT_ERR:
return .module_init
case NSCRIPT_PARSE_ERR:
return .parse
default:
return .parse
}
}
func resume(with: NScriptResumeWith) -> NostrScriptRunResult? {
guard let runstate, runstate.is_suspended, can_resume(with: with) else {
return nil
}
switch with {
case .event(let resp):
load_data(resp: resp)
}
let st = nscript_run(interp: &interp, resuming: true)
self.runstate = st
self.event = nil
return st
}
private func load_data(resp: NostrResponse) {
self.event = resp
}
func run() -> NostrScriptRunResult {
if let runstate {
return runstate
}
let st = nscript_run(interp: &interp, resuming: false)
self.runstate = st
return st
}
}
fileprivate func interp_nostrscript(interp: UnsafeMutablePointer<wasm_interp>?) -> NostrScript? {
guard let interp = interp?.pointee else {
return nil
}
return Unmanaged<NostrScript>.fromOpaque(interp.context).takeUnretainedValue()
}
fileprivate func asm_str_byteptr(cstr: UnsafePointer<UInt8>, len: Int32) -> String? {
let u16 = cstr.withMemoryRebound(to: UInt16.self, capacity: Int(len)) { p in p }
return asm_str(cstr: u16, len: len)
}
fileprivate func asm_str(cstr: UnsafePointer<UInt16>, len: Int32) -> String? {
return String(utf16CodeUnits: cstr, count: Int(len))
}
enum NScriptCommand: Int {
case pool_send = 1
case add_relay = 2
case event_await = 3
case event_get_type = 4
case event_get_note = 5
case note_get_kind = 6
case note_get_content = 7
case note_get_content_length = 8
}
enum NScriptEventType: Int {
case ok = 1
case note = 2
case notice = 3
case eose = 4
case auth = 5
init(resp: NostrResponse) {
switch resp {
case .event:
self = .note
case .notice:
self = .notice
case .eose:
self = .eose
case .ok:
self = .ok
case .auth:
self = .auth
}
}
}
enum NScriptWaiting: Equatable {
case event(String)
var subid: String {
switch self {
case .event(let subid):
return subid
}
}
}
enum NScriptResumeWith {
case event(NostrResponse)
}
enum NScriptCmdResult {
case suspend(NScriptWaiting)
case ok
case fatal
}
@_cdecl("nscript_nostr_cmd")
public func nscript_nostr_cmd(interp: UnsafeMutablePointer<wasm_interp>?, cmd: Int32, value: UnsafePointer<UInt8>, len: Int32) -> Int32 {
guard let script = interp_nostrscript(interp: interp),
let cmd = NScriptCommand(rawValue: Int(cmd)) else {
return 0
}
print("nostr_cmd \(cmd)")
switch cmd {
case .pool_send:
guard let req = asm_str_byteptr(cstr: value, len: len) else { return 0 }
let res = nscript_pool_send(script: script, req: req)
stack_push_i32(interp, 0);
return res;
case .add_relay:
guard let relay = asm_str_byteptr(cstr: value, len: len) else { return 0 }
let ok = nscript_add_relay(script: script, relay: relay)
stack_push_i32(interp, ok ? 1 : 0)
return 1;
case .event_await:
guard let subid = asm_str_byteptr(cstr: value, len: len) else { return 0 }
nscript_event_await(script: script, subid: subid)
let ev_handle: Int32 = 1
stack_push_i32(interp, ev_handle);
return BUILTIN_SUSPEND
case .event_get_type:
guard let event = script.event else {
stack_push_i32(interp, 0);
return 1
}
let type = NScriptEventType(resp: event)
stack_push_i32(interp, Int32(type.rawValue));
return 1
case .event_get_note:
guard let event = script.event, case .event = event
else { stack_push_i32(interp, 0); return 1 }
let note_handle: Int32 = 1
stack_push_i32(interp, note_handle)
return 1
case .note_get_kind:
guard let event = script.event, case .event(_, let note) = event
else {
stack_push_i32(interp, 0);
return 1
}
stack_push_i32(interp, Int32(note.kind))
return 1
case .note_get_content:
guard let event = script.event, case .event(_, let note) = event
else { stack_push_i32(interp, 0); return 1 }
stack_push_i32(interp, Int32(note.kind))
return 1
case .note_get_content_length:
guard let event = script.event, case .event(_, let note) = event
else { stack_push_i32(interp, 0); return 1 }
stack_push_i32(interp, Int32(note.content.utf8.count))
return 1
}
}
func nscript_add_relay(script: NostrScript, relay: String) -> Bool {
guard let url = RelayURL(relay) else { return false }
let desc = RelayDescriptor(url: url, info: .rw, variant: .ephemeral)
return (try? script.pool.add_relay(desc)) != nil
}
@_cdecl("nscript_set_bool")
public func nscript_set_bool(interp: UnsafeMutablePointer<wasm_interp>?, setting: UnsafePointer<UInt16>, setting_len: Int32, val: Int32) -> Int32 {
guard let setting = asm_str(cstr: setting, len: setting_len),
UserSettingsStore.bool_options.contains(setting)
else {
stack_push_i32(interp, 0);
return 1;
}
let key = pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: setting)
let b = val > 0 ? true : false
print("nscript setting bool setting \(setting) to \(b)")
UserDefaults.standard.set(b, forKey: key)
stack_push_i32(interp, 1);
return 1;
}
@_cdecl("nscript_pool_send_to")
public func nscript_pool_send_to(interp: UnsafeMutablePointer<wasm_interp>?, preq: UnsafePointer<UInt16>, req_len: Int32, to: UnsafePointer<UInt16>, to_len: Int32) -> Int32 {
guard let script = interp_nostrscript(interp: interp),
let req_str = asm_str(cstr: preq, len: req_len),
let to = asm_str(cstr: to, len: to_len),
let to_relay_url = RelayURL(to)
else {
return 0
}
DispatchQueue.main.async {
script.pool.send_raw(.custom(req_str), to: [to_relay_url], skip_ephemeral: false)
}
return 1;
}
func nscript_pool_send(script: NostrScript, req req_str: String) -> Int32 {
//script.test("pool_send: '\(req_str)'")
DispatchQueue.main.sync {
script.pool.send_raw(.custom(req_str), skip_ephemeral: false)
}
return 1;
}
func nscript_event_await(script: NostrScript, subid: String) {
script.waiting_on = .event(subid)
}
func nscript_get_error_backtrace(errors: inout errors) -> [String] {
var xs = [String]()
var errs = cursor()
var err = error()
copy_cursor(&errors.cur, &errs)
errs.p = errs.start;
while (errs.p < errors.cur.p) {
if (cursor_pull_error(&errs, &err) == 0) {
return xs
}
xs.append(String(cString: err.msg))
}
return xs
}
func nscript_run(interp: inout wasm_interp, resuming: Bool) -> NostrScriptRunResult {
var res: Int32 = 0
var retval: Int32 = 0
if (resuming) {
print("resuming nostrscript");
res = interp_wasm_module_resume(&interp, &retval);
} else {
res = interp_wasm_module(&interp, &retval);
}
if res == 0 {
print_callstack(&interp);
print_error_backtrace(&interp.errors);
let backtrace = nscript_get_error_backtrace(errors: &interp.errors)
return .runtime_err(backtrace)
}
if res == BUILTIN_SUSPEND {
return .suspend
}
//print_stack(&interp.stack);
wasm_interp_free(&interp);
return .finished(Int(retval))
}