Files
damus/damus/Core/Nostr/NostrResponse.swift
T
Daniel D’Aquino 95d38fa802 Implement initial negentropy base functions
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>
2026-01-22 14:20:57 -08:00

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)
}