95d38fa802
This implements some useful functions to use negentropy from RelayPool, but does not integrate them with the rest of the app. No changelog for the negentropy support right now as it is not hooked up to any user-facing feature Changelog-Fixed: Fixed a race condition in the networking logic that could cause notes to get missed in certain rare scenarios Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
214 lines
7.4 KiB
Swift
214 lines
7.4 KiB
Swift
//
|
|
// NostrResponse.swift
|
|
// damus
|
|
//
|
|
// Created by William Casarin on 2022-04-11.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
struct CommandResult {
|
|
let event_id: NoteId
|
|
let ok: Bool
|
|
let msg: String
|
|
}
|
|
|
|
enum MaybeResponse {
|
|
case bad
|
|
case ok(NostrResponse)
|
|
}
|
|
|
|
enum NegentropyResponse {
|
|
/// Negentropy error
|
|
case error(subscriptionId: String, reasonCodeString: String)
|
|
/// Negentropy message
|
|
case message(subscriptionId: String, data: [UInt8])
|
|
/// Invalid negentropy message
|
|
case invalidResponse(subscriptionId: String)
|
|
|
|
var subscriptionId: String {
|
|
switch self {
|
|
case .error(subscriptionId: let subscriptionId, reasonCodeString: let reasonCodeString): subscriptionId
|
|
case .message(subscriptionId: let subscriptionId, data: let data): subscriptionId
|
|
case .invalidResponse(subscriptionId: let subscriptionId): subscriptionId
|
|
}
|
|
}
|
|
}
|
|
|
|
enum NostrResponse {
|
|
case event(String, NostrEvent)
|
|
case notice(String)
|
|
case eose(String)
|
|
case ok(CommandResult)
|
|
/// An [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md) `auth` challenge.
|
|
///
|
|
/// The associated type of this case is the challenge string sent by the server.
|
|
case auth(String)
|
|
/// Negentropy error
|
|
case negentropyError(subscriptionId: String, reasonCodeString: String)
|
|
/// Negentropy message
|
|
case negentropyMessage(subscriptionId: String, hexEncodedData: String)
|
|
|
|
var subid: String? {
|
|
switch self {
|
|
case .ok:
|
|
return nil
|
|
case .event(let sub_id, _):
|
|
return sub_id
|
|
case .eose(let sub_id):
|
|
return sub_id
|
|
case .notice(_):
|
|
return nil
|
|
case .auth(let challenge_string):
|
|
return challenge_string
|
|
case .negentropyError(subscriptionId: let subscriptionId, reasonCodeString: _):
|
|
return subscriptionId
|
|
case .negentropyMessage(subscriptionId: let subscriptionId, hexEncodedData: _):
|
|
return subscriptionId
|
|
}
|
|
}
|
|
|
|
var negentropyResponse: NegentropyResponse? {
|
|
switch self {
|
|
case .event(_, _): return nil
|
|
case .notice(_): return nil
|
|
case .eose(_): return nil
|
|
case .ok(_): return nil
|
|
case .auth(_): return nil
|
|
case .negentropyError(subscriptionId: let subscriptionId, reasonCodeString: let reasonCodeString):
|
|
return .error(subscriptionId: subscriptionId, reasonCodeString: reasonCodeString)
|
|
case .negentropyMessage(subscriptionId: let subscriptionId, hexEncodedData: let hexData):
|
|
if let bytes = hex_decode(hexData) {
|
|
return .message(subscriptionId: subscriptionId, data: bytes)
|
|
}
|
|
return .invalidResponse(subscriptionId: subscriptionId)
|
|
}
|
|
}
|
|
|
|
/// Decode a Nostr response from JSON using idiomatic Swift parsing
|
|
/// Supports NEG-MSG and NEG-ERR formats, falling back to C parsing for other message types
|
|
static func decode(from json: String) -> NostrResponse? {
|
|
// Try Swift-based parsing first for negentropy messages
|
|
if let response = try? decodeNegentropyMessage(from: json) {
|
|
return response
|
|
}
|
|
|
|
// Fall back to C-based parsing for standard Nostr messages
|
|
return owned_from_json(json: json)
|
|
}
|
|
|
|
/// Decode negentropy messages using idiomatic Swift
|
|
private static func decodeNegentropyMessage(from json: String) throws -> NostrResponse? {
|
|
guard let jsonData = json.data(using: .utf8) else {
|
|
return nil
|
|
}
|
|
|
|
guard let jsonArray = try JSONSerialization.jsonObject(with: jsonData) as? [Any],
|
|
jsonArray.count >= 2,
|
|
let messageType = jsonArray[0] as? String else {
|
|
return nil
|
|
}
|
|
|
|
switch messageType {
|
|
case "NEG-MSG":
|
|
// Format: ["NEG-MSG", "subscription-id", "hex-encoded-data"]
|
|
guard jsonArray.count == 3,
|
|
let subscriptionId = jsonArray[1] as? String,
|
|
let hexData = jsonArray[2] as? String else {
|
|
return nil
|
|
}
|
|
return .negentropyMessage(subscriptionId: subscriptionId, hexEncodedData: hexData)
|
|
|
|
case "NEG-ERR":
|
|
// Format: ["NEG-ERR", "subscription-id", "reason-code"]
|
|
guard jsonArray.count == 3,
|
|
let subscriptionId = jsonArray[1] as? String,
|
|
let reasonCode = jsonArray[2] as? String else {
|
|
return nil
|
|
}
|
|
return .negentropyError(subscriptionId: subscriptionId, reasonCodeString: reasonCode)
|
|
|
|
default:
|
|
// Not a negentropy message
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private static func owned_from_json(json: String) -> NostrResponse? {
|
|
return json.withCString{ cstr in
|
|
let bufsize: Int = max(Int(Double(json.utf8.count) * 8.0), Int(getpagesize()))
|
|
let data = malloc(bufsize)
|
|
|
|
if data == nil {
|
|
let r: NostrResponse? = nil
|
|
return r
|
|
}
|
|
//guard var json_cstr = json.cString(using: .utf8) else { return nil }
|
|
|
|
//json_cs
|
|
var tce = ndb_tce()
|
|
|
|
let len = ndb_ws_event_from_json(cstr, Int32(json.utf8.count), &tce, data, Int32(bufsize), nil)
|
|
if len <= 0 {
|
|
free(data)
|
|
return nil
|
|
}
|
|
|
|
switch tce.evtype {
|
|
case NDB_TCE_OK:
|
|
defer { free(data) }
|
|
|
|
guard let evid_str = sized_cstr(cstr: tce.subid, len: tce.subid_len),
|
|
let evid = hex_decode_noteid(evid_str),
|
|
let msg = sized_cstr(cstr: tce.command_result.msg, len: tce.command_result.msglen) else {
|
|
return nil
|
|
}
|
|
let cr = CommandResult(event_id: evid, ok: tce.command_result.ok == 1, msg: msg)
|
|
|
|
return .ok(cr)
|
|
case NDB_TCE_EOSE:
|
|
defer { free(data) }
|
|
|
|
guard let subid = sized_cstr(cstr: tce.subid, len: tce.subid_len) else {
|
|
return nil
|
|
}
|
|
return .eose(subid)
|
|
case NDB_TCE_EVENT:
|
|
|
|
// Create new Data with just the valid bytes
|
|
guard let note_data = realloc(data, Int(len)) else {
|
|
free(data)
|
|
return nil
|
|
}
|
|
let new_note = ndb_note_ptr(ptr: OpaquePointer(note_data))
|
|
let note = NdbNote(note: new_note, size: Int(len), owned: true, key: nil)
|
|
|
|
guard let subid = sized_cstr(cstr: tce.subid, len: tce.subid_len) else {
|
|
free(data)
|
|
return nil
|
|
}
|
|
return .event(subid, note)
|
|
case NDB_TCE_NOTICE:
|
|
free(data)
|
|
return .notice("")
|
|
case NDB_TCE_AUTH:
|
|
defer { free(data) }
|
|
|
|
guard let challenge_string = sized_cstr(cstr: tce.subid, len: tce.subid_len) else {
|
|
return nil
|
|
}
|
|
return .auth(challenge_string)
|
|
default:
|
|
free(data)
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func sized_cstr(cstr: UnsafePointer<CChar>, len: Int32) -> String? {
|
|
let msgbuf = Data(bytes: cstr, count: Int(len))
|
|
return String(data: msgbuf, encoding: .utf8)
|
|
}
|
|
|