ndb: switch to nostrdb notes

This is a refactor of the codebase to use a more memory-efficient
representation of notes. It should also be much faster at decoding since
we're using a custom C json parser now.

Changelog-Changed: Improved memory usage and performance when processing events
This commit is contained in:
William Casarin
2023-07-26 08:46:44 -07:00
parent 55bbe8f855
commit cebd1f48ca
110 changed files with 2153 additions and 1799 deletions

116
damus/Nostr/Id.swift Normal file
View File

@@ -0,0 +1,116 @@
//
// Id.swift
// damus
//
// Created by William Casarin on 2023-07-26.
//
import Foundation
struct TagRef<T>: Hashable, Equatable, Encodable {
let elem: TagElem
init(_ elem: TagElem) {
self.elem = elem
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(elem.string())
}
}
protocol TagKey {
var keychar: AsciiCharacter { get }
}
protocol TagKeys {
associatedtype TagKeys: TagKey
var key: TagKeys { get }
}
protocol TagConvertible {
var tag: [String] { get }
static func from_tag(tag: TagSequence) -> Self?
}
struct QuoteId: IdType, TagKey {
let id: Data
init(_ data: Data) {
self.id = data
}
var keychar: AsciiCharacter { "q" }
}
struct Privkey: IdType {
let id: Data
var nsec: String {
bech32_privkey(self)
}
init?(hex: String) {
guard let id = hex_decode_id(hex) else {
return nil
}
self.init(id)
}
init(_ data: Data) {
self.id = data
}
}
struct Hashtag: TagConvertible {
let hashtag: String
static func from_tag(tag: TagSequence) -> Hashtag? {
var i = tag.makeIterator()
guard tag.count >= 2,
let t0 = i.next(),
let chr = t0.single_char,
chr == "t",
let t1 = i.next() else {
return nil
}
return Hashtag(hashtag: t1.string())
}
var tag: [String] { ["t", self.hashtag] }
var keychar: AsciiCharacter { "t" }
}
struct ReplaceableParam: TagConvertible {
let param: TagElem
static func from_tag(tag: TagSequence) -> ReplaceableParam? {
var i = tag.makeIterator()
guard tag.count >= 2,
let t0 = i.next(),
let chr = t0.single_char,
chr == "d",
let t1 = i.next() else {
return nil
}
return ReplaceableParam(param: t1)
}
var tag: [String] { [self.keychar.description, self.param.string()] }
var keychar: AsciiCharacter { "d" }
}
struct Signature: Hashable, Equatable {
let data: Data
init(_ p: Data) {
self.data = p
}
}

View File

@@ -20,16 +20,20 @@ enum ValidationResult: Decodable {
case bad_sig
}
//typealias NostrEvent = NdbNote
//typealias Tags = TagsSequence
typealias Tags = [[String]]
typealias NostrEvent = NostrEventOld
typealias NostrEvent = NdbNote
typealias TagElem = NdbTagElem
typealias Tag = TagSequence
typealias Tags = TagsSequence
//typealias TagElem = String
//typealias Tag = [TagElem]
//typealias Tags = [Tag]
//typealias NostrEvent = NostrEventOld
let MAX_NOTE_SIZE: Int = 2 << 18
/*
class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable, Hashable, Comparable {
// TODO: memory mapped db events
/*
private var note_data: UnsafeMutablePointer<ndb_note>
init(data: UnsafeMutablePointer<ndb_note>) {
@@ -51,7 +55,6 @@ class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable,
}
var tags: TagIterator
*/
let id: String
let content: String
@@ -90,12 +93,16 @@ class NostrEventOld: Codable, Identifiable, CustomStringConvertible, Equatable,
hasher.combine(id)
}
static func owned_from_json(json: String) -> NostrEvent? {
private enum CodingKeys: String, CodingKey {
case id, sig, tags, pubkey, created_at, kind, content
}
static func owned_from_json(json: String) -> NostrEventOld? {
let decoder = JSONDecoder()
guard let dat = json.data(using: .utf8) else {
return nil
}
guard let ev = try? decoder.decode(NostrEvent.self, from: dat) else {
guard let ev = try? decoder.decode(NostrEventOld.self, from: dat) else {
return nil
}
@@ -214,10 +221,6 @@ extension NostrEventOld {
return NostrKind.init(rawValue: kind)
}
private enum CodingKeys: String, CodingKey {
case id, sig, tags, pubkey, created_at, kind, content
}
private func get_referenced_ids(key: String) -> [ReferencedId] {
return damus.get_referenced_ids(tags: self.tags, key: key)
}
@@ -309,6 +312,7 @@ extension NostrEventOld {
return Date.now.timeIntervalSince(event_date)
}
}
*/
func sign_id(privkey: String, id: String) -> String {
let priv_key_bytes = try! privkey.bytes
@@ -316,7 +320,7 @@ func sign_id(privkey: String, id: String) -> String {
// Extra params for custom signing
var aux_rand = random_bytes(count: 64)
var aux_rand = random_bytes(count: 64).bytes
var digest = try! id.bytes
// API allows for signing variable length messages
@@ -326,7 +330,7 @@ func sign_id(privkey: String, id: String) -> String {
}
func decode_nostr_event(txt: String) -> NostrResponse? {
return decode_data(Data(txt.utf8))
return NostrResponse.owned_from_json(json: txt)
}
func encode_json<T: Encodable>(_ val: T) -> String? {
@@ -336,7 +340,7 @@ func encode_json<T: Encodable>(_ val: T) -> String? {
}
func decode_nostr_event_json(json: String) -> NostrEvent? {
return decode_json(json)
return NostrEvent.owned_from_json(json: json)
}
/*
@@ -390,7 +394,7 @@ func event_commitment(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[
let tags_data = try! tags_encoder.encode(tags)
let tags = String(decoding: tags_data, as: UTF8.self)
return "[0,\"\(pubkey)\",\(created_at),\(kind),\(tags),\(content)]"
return "[0,\"\(pubkey.hex())\",\(created_at),\(kind),\(tags),\(content)]"
}
func calculate_event_commitment(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> Data {
@@ -398,9 +402,9 @@ func calculate_event_commitment(pubkey: Pubkey, created_at: UInt32, kind: UInt32
return target.data(using: .utf8)!
}
func calculate_event_id(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> Data {
func calculate_event_id(pubkey: Pubkey, created_at: UInt32, kind: UInt32, tags: [[String]], content: String) -> NoteId {
let commitment = calculate_event_commitment(pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content)
return sha256(commitment)
return NoteId(sha256(commitment))
}
@@ -436,8 +440,6 @@ func hex_encode(_ data: Data) -> String {
return str
}
func random_bytes(count: Int) -> Data {
var bytes = [Int8](repeating: 0, count: count)
guard
@@ -448,42 +450,6 @@ func random_bytes(count: Int) -> Data {
return Data(bytes: bytes, count: count)
}
func refid_to_tag(_ ref: ReferencedId) -> [String] {
var tag = [ref.key, ref.ref_id]
if let relay_id = ref.relay_id {
tag.append(relay_id)
}
return tag
}
func tag_to_refid(_ tag: [String]) -> ReferencedId? {
if tag.count == 0 {
return nil
}
if tag.count == 1 {
return nil
}
var relay_id: String? = nil
if tag.count > 2 {
relay_id = tag[2]
}
return ReferencedId(ref_id: tag[1], relay_id: relay_id, key: tag[0])
}
func get_referenced_ids(tags: [[String]], key: String) -> [ReferencedId] {
return tags.reduce(into: []) { (acc, tag) in
if tag.count >= 2 && tag[0] == key {
var relay_id: String? = nil
if tag.count >= 3 {
relay_id = tag[2]
}
acc.append(ReferencedId(ref_id: tag[1], relay_id: relay_id, key: key))
}
}
}
func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
let rw_relay_info = RelayInfo(read: true, write: true)
@@ -511,18 +477,33 @@ func make_metadata_event(keypair: FullKeypair, metadata: Profile) -> NostrEvent?
}
func make_boost_event(keypair: FullKeypair, boosted: NostrEvent) -> NostrEvent? {
var tags: [[String]] = boosted.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
tags.append(["e", boosted.id, "", "root"])
var tags = boosted.tags.reduce(into: [[String]]()) { ts, tag in
guard tag.count >= 2 && (tag[0].matches_char("e") || tag[0].matches_char("p")) else {
return
}
ts.append(tag.strings())
}
tags.append(["e", boosted.id.hex(), "", "root"])
tags.append(["p", boosted.pubkey.hex()])
return NostrEvent(content: event_to_json(ev: boosted), keypair: keypair.to_keypair(), kind: 6, tags: tags)
let content = boosted.content_len <= 100 ? event_to_json(ev: boosted) : ""
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 6, tags: tags)
}
func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙") -> NostrEvent? {
var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
var tags = liked.tags.reduce(into: [[String]]()) { ts, tag in
guard tag.count >= 2,
(tag[0].matches_char("e") || tag[0].matches_char("p")) else {
return
}
ts.append(tag.strings())
}
tags.append(["e", liked.id.hex()])
tags.append(["p", liked.pubkey.hex()])
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags)
}
@@ -556,17 +537,19 @@ func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair,
}
func decrypt_private_zap(our_privkey: Privkey, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? {
guard let anon_tag = zapreq.tags.first(where: { t in t.count >= 2 && t[0] == "anon" }) else {
guard let anon_tag = zapreq.tags.first(where: { t in
t.count >= 2 && t[0].matches_str("anon")
}) else {
return nil
}
let enc_note = anon_tag[1]
let enc_note = anon_tag[1].string()
var note = decrypt_note(our_privkey: our_privkey, their_pubkey: zapreq.pubkey, enc_note: enc_note, encoding: .bech32)
// check to see if the private note was from us
if note == nil {
guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: target.id, created_at: zapreq.created_at) else {
guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: NoteId(target.id), created_at: zapreq.created_at) else {
return nil
}
// use our private keypair and their pubkey to get the shared secret
@@ -602,17 +585,15 @@ func decrypt_private_zap(our_privkey: Privkey, zapreq: NostrEvent, target: ZapTa
return note
}
func generate_private_keypair(our_privkey: String, id: String, created_at: UInt32) -> FullKeypair? {
let to_hash = our_privkey + id + String(created_at)
func generate_private_keypair(our_privkey: Privkey, id: NoteId, created_at: UInt32) -> FullKeypair? {
let to_hash = our_privkey.hex() + id.hex() + String(created_at)
guard let dat = to_hash.data(using: .utf8) else {
return nil
}
let privkey_bytes = sha256(dat)
let privkey = hex_encode(privkey_bytes)
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
return nil
}
let privkey = Privkey(privkey_bytes)
guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil }
return FullKeypair(pubkey: pubkey, privkey: privkey)
}
@@ -661,7 +642,7 @@ func make_zap_request_event(keypair: FullKeypair, content: String, relays: [Rela
tags.append(["anon"])
kp = generate_new_keypair()
case .priv:
guard let priv_kp = generate_private_keypair(our_privkey: keypair.privkey, id: target.id, created_at: now) else {
guard let priv_kp = generate_private_keypair(our_privkey: keypair.privkey, id: NoteId(target.id), created_at: now) else {
return nil
}
kp = priv_kp
@@ -699,27 +680,36 @@ func uniq<T: Hashable>(_ xs: [T]) -> [T] {
return ys
}
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
func gather_reply_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] {
var ids: [RefId] = from.referenced_ids.first.map({ ref in [ .event(ref) ] }) ?? []
ids.append(.e(from.id))
ids.append(contentsOf: uniq(from.referenced_pubkeys.filter { $0.ref_id != our_pubkey }))
if from.pubkey != our_pubkey {
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
let pks = from.referenced_pubkeys.reduce(into: [RefId]()) { rs, pk in
if pk == our_pubkey {
return
}
rs.append(.pubkey(pk))
}
ids.append(.event(from.id))
ids.append(contentsOf: uniq(pks))
if from.pubkey != our_pubkey {
ids.append(.pubkey(from.pubkey))
}
return ids
}
func gather_quote_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids: [ReferencedId] = [.q(from.id)]
func gather_quote_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] {
var ids: [RefId] = [.quote(from.id.quote_id)]
if from.pubkey != our_pubkey {
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
ids.append(.pubkey(from.pubkey))
}
return ids
}
func event_from_json(dat: String) -> NostrEvent? {
return try? JSONDecoder().decode(NostrEvent.self, from: Data(dat.utf8))
return NostrEvent.owned_from_json(json: dat)
}
func event_to_json(ev: NostrEvent) -> String {
@@ -757,13 +747,10 @@ func decrypt_note(our_privkey: Privkey, their_pubkey: Pubkey, enc_note: String,
return decode_nostr_event_json(json: dec)
}
func get_shared_secret(privkey: String, pubkey: String) -> [UInt8]? {
guard let privkey_bytes = try? privkey.bytes else {
return nil
}
guard var pk_bytes = try? pubkey.bytes else {
return nil
}
func get_shared_secret(privkey: Privkey, pubkey: Pubkey) -> [UInt8]? {
let privkey_bytes = privkey.bytes
var pk_bytes = pubkey.bytes
pk_bytes.insert(2, at: 0)
var publicKey = secp256k1_pubkey()
@@ -924,45 +911,33 @@ func aes_operation(operation: CCOperation, data: [UInt8], iv: [UInt8], shared_se
func validate_event(ev: NostrEvent) -> ValidationResult {
let raw_id = calculate_event_id(pubkey: ev.pubkey, created_at: ev.created_at, kind: ev.kind, tags: ev.tags, content: ev.content)
let id = hex_encode(raw_id)
let id = calculate_event_id(pubkey: ev.pubkey, created_at: ev.created_at, kind: ev.kind, tags: ev.tags.strings(), content: ev.content)
if id != ev.id {
return .bad_id
}
// TODO: implement verify
guard var sig64 = hex_decode(ev.sig)?.bytes else {
return .bad_sig
}
guard var ev_pubkey = hex_decode(ev.pubkey)?.bytes else {
return .bad_sig
}
let ctx = secp256k1.Context.raw
var xonly_pubkey = secp256k1_xonly_pubkey.init()
var ev_pubkey = ev.pubkey.id.bytes
var ok = secp256k1_xonly_pubkey_parse(ctx, &xonly_pubkey, &ev_pubkey) != 0
if !ok {
return .bad_sig
}
var raw_id_bytes = raw_id.bytes
ok = secp256k1_schnorrsig_verify(ctx, &sig64, &raw_id_bytes, raw_id.count, &xonly_pubkey) > 0
var sig = ev.sig.data.bytes
var idbytes = id.id.bytes
ok = secp256k1_schnorrsig_verify(ctx, &sig, &idbytes, 32, &xonly_pubkey) > 0
return ok ? .ok : .bad_sig
}
func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
func first_eref_mention(ev: NostrEvent, privkey: Privkey?) -> Mention<NoteId>? {
let blocks = ev.blocks(privkey).blocks.filter { block in
guard case .mention(let mention) = block else {
return false
}
guard case .event = mention.type else {
return false
}
if mention.ref.key != "e" {
guard case .mention(let mention) = block,
case .note = mention.ref else {
return false
}
@@ -970,10 +945,13 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
}
/// MARK: - Preview
if let firstBlock = blocks.first, case .mention(let mention) = firstBlock, mention.ref.key == "e" {
return mention
if let firstBlock = blocks.first,
case .mention(let mention) = firstBlock,
case .note(let note_id) = mention.ref
{
return .note(note_id)
}
return nil
}
@@ -999,20 +977,3 @@ func to_reaction_emoji(ev: NostrEvent) -> String? {
}
}
extension [ReferencedId] {
var pRefs: [ReferencedId] {
get {
Set(self).filter { ref in
ref.key == "p"
}
}
}
var eRefs: [ReferencedId] {
get {
self.filter { ref in
ref.key == "e"
}
}
}
}

View File

@@ -16,8 +16,6 @@ enum NostrKind: UInt32, Codable {
case delete = 5
case boost = 6
case like = 7
case channel_create = 40
case channel_meta = 41
case chat = 42
case list = 30000
case longform = 30023

View File

@@ -9,18 +9,18 @@ import Foundation
enum NostrLink: Equatable {
case ref(ReferencedId)
case ref(RefId)
case filter(NostrFilter)
case script([UInt8])
}
func encode_pubkey_uri(_ ref: ReferencedId) -> String {
return "p:" + ref.ref_id
func encode_pubkey_uri(_ pubkey: Pubkey) -> String {
return "p:" + pubkey.hex()
}
// TODO: bech32 and relay hints
func encode_event_id_uri(_ ref: ReferencedId) -> String {
return "e:" + ref.ref_id
func encode_event_id_uri(_ noteid: NoteId) -> String {
return "e:" + noteid.hex()
}
func parse_nostr_ref_uri_type(_ p: Parser) -> String? {
@@ -55,36 +55,21 @@ func parse_hexstr(_ p: Parser, len: Int) -> String? {
return String(substring(p.str, start: start, end: p.pos))
}
func parse_nostr_ref_uri(_ p: Parser) -> ReferencedId? {
let start = p.pos
if !parse_str(p, "nostr:") {
return nil
}
guard let ref = parse_post_bech32_mention(p) else {
p.pos = start
return nil
}
return ref
}
func decode_universal_link(_ s: String) -> NostrLink? {
var uri = s.replacingOccurrences(of: "https://damus.io/r/", with: "")
uri = uri.replacingOccurrences(of: "https://damus.io/", with: "")
uri = uri.replacingOccurrences(of: "/", with: "")
guard let decoded = try? bech32_decode(uri) else {
guard let decoded = try? bech32_decode(uri),
decoded.data.count == 32
else {
return nil
}
let h = hex_encode(decoded.data)
if decoded.hrp == "note" {
return .ref(ReferencedId(ref_id: h, relay_id: nil, key: "e"))
return .ref(.event(NoteId(decoded.data)))
} else if decoded.hrp == "npub" {
return .ref(ReferencedId(ref_id: h, relay_id: nil, key: "p"))
return .ref(.pubkey(Pubkey(decoded.data)))
}
// TODO: handle nprofile, etc
@@ -98,14 +83,12 @@ func decode_nostr_bech32_uri(_ s: String) -> NostrLink? {
switch obj {
case .nsec(let privkey):
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
return nil
}
return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"))
guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil }
return .ref(.pubkey(pubkey))
case .npub(let pubkey):
return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"))
return .ref(.pubkey(pubkey))
case .note(let id):
return .ref(ReferencedId(ref_id: id, relay_id: nil, key: "e"))
return .ref(.event(id))
case .nscript(let data):
return .script(data)
}
@@ -134,19 +117,15 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
acc.append(decoded)
return
}
if tag_is_hashtag(parts) {
if parts.count >= 2 && parts[0] == "t" {
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
}
if let rid = tag_to_refid(parts) {
return .ref(rid)
}
guard parts.count == 1 else {
return nil
}
let part = parts[0]
return decode_nostr_bech32_uri(part)

View File

@@ -13,7 +13,12 @@ struct CommandResult {
let msg: String
}
enum NostrResponse: Decodable {
enum MaybeResponse {
case bad
case ok(NostrResponse)
}
enum NostrResponse {
case event(String, NostrEvent)
case notice(String)
case eose(String)
@@ -32,48 +37,73 @@ enum NostrResponse: Decodable {
}
}
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
static func owned_from_json(json: String) -> NostrResponse? {
return json.withCString{ cstr in
let bufsize: Int = max(Int(Double(json.utf8.count) * 2.0), Int(getpagesize()))
let data = malloc(bufsize)
// Only use first item
let typ = try container.decode(String.self)
if typ == "EVENT" {
let sub_id = try container.decode(String.self)
var ev: NostrEvent
do {
ev = try container.decode(NostrEvent.self)
} catch {
print(error)
throw error
if data == nil {
let r: NostrResponse? = nil
return r
}
//ev.pow = count_hash_leading_zero_bits(ev.id)
self = .event(sub_id, ev)
return
} else if typ == "NOTICE" {
let msg = try container.decode(String.self)
self = .notice(msg)
return
} else if typ == "EOSE" {
let sub_id = try container.decode(String.self)
self = .eose(sub_id)
return
} else if typ == "OK" {
var cr: CommandResult
do {
let event_id = try container.decode(String.self)
let ok = try container.decode(Bool.self)
let msg = try container.decode(String.self)
cr = CommandResult(event_id: event_id, ok: ok, msg: msg)
} catch {
print(error)
throw error
//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))
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 = note_data.assumingMemoryBound(to: ndb_note.self)
let note = NdbNote(note: new_note, owned_size: Int(len))
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("")
default:
free(data)
return nil
}
self = .ok(cr)
return
//ev.pow = count_hash_leading_zero_bits(ev.id)
}
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT, NOTICE or OK, got \(typ)"))
}
}
func sized_cstr(cstr: UnsafePointer<CChar>, len: Int32) -> String? {
let msgbuf = Data(bytes: cstr, count: Int(len))
return String(data: msgbuf, encoding: .utf8)
}

View File

@@ -55,3 +55,22 @@ func hex_decode(_ str: String) -> [UInt8]?
}
func hex_decode_id(_ str: String) -> Data? {
guard str.utf8.count == 64, let decoded = hex_decode(str) else {
return nil
}
return Data(decoded)
}
func hex_decode_noteid(_ str: String) -> NoteId? {
return hex_decode_id(str).map(NoteId.init)
}
func hex_decode_pubkey(_ str: String) -> Pubkey? {
return hex_decode_id(str).map(Pubkey.init)
}
func hex_decode_privkey(_ str: String) -> Privkey? {
return hex_decode_id(str).map(Privkey.init)
}

View File

@@ -1,29 +0,0 @@
//
// Pubkey.swift
// damus
//
// Created by William Casarin on 2023-07-30.
//
import Foundation
// prepare a more gradual transition to the ndb branch
typealias FollowRef = ReferencedId
typealias Pubkey = String
typealias NoteId = String
typealias Privkey = String
extension String {
// Id constructors
init?(hex: String) {
self = hex
}
static var empty: String {
return ""
}
func hex() -> String {
return self
}
}

View File

@@ -7,79 +7,44 @@
import Foundation
struct Reference {
let key: AsciiCharacter
let id: NdbTagElem
var ref_id: NdbTagElem {
id
}
func to_referenced_id() -> ReferencedId {
ReferencedId(ref_id: id.string(), relay_id: nil, key: key.description)
}
}
func tagref_should_be_id(_ tag: NdbTagElem) -> Bool {
return !tag.matches_char("t")
return !(tag.matches_char("t") || tag.matches_char("d"))
}
struct References: Sequence, IteratorProtocol {
struct References<T: TagConvertible>: Sequence, IteratorProtocol {
let tags: TagsSequence
var tags_iter: TagsIterator
mutating func next() -> Reference? {
while let tag = tags_iter.next() {
guard tag.count >= 2 else { continue }
let key = tag[0]
let id = tag[1]
guard key.count == 1, tagref_should_be_id(id) else { continue }
for c in key {
guard let a = AsciiCharacter(c) else { break }
return Reference(key: a, id: id)
}
}
return nil
}
static func ids(tags: TagsSequence) -> LazyFilterSequence<References> {
References(tags: tags).lazy
.filter() { ref in ref.key == "e" }
}
static func pubkeys(tags: TagsSequence) -> LazyFilterSequence<References> {
References(tags: tags).lazy
.filter() { ref in ref.key == "p" }
}
static func hashtags(tags: TagsSequence) -> LazyFilterSequence<References> {
References(tags: tags).lazy
.filter() { ref in ref.key == "t" }
}
init(tags: TagsSequence) {
self.tags = tags
self.tags_iter = tags.makeIterator()
}
}
// TagsSequence transition helpers
extension [[String]] {
func strings() -> [[String]] {
return self
mutating func next() -> T? {
while let tag = tags_iter.next() {
guard let evref = T.from_tag(tag: tag) else { continue }
return evref
}
return nil
}
}
// TagsSequence transition helpers
extension [String] {
func strings() -> [String] {
return self
extension References {
var first: T? {
self.first(where: { _ in true })
}
var last: T? {
var last: T? = nil
for t in self {
last = t
}
return last
}
}
// NdbTagElem transition helpers
extension String {
func string() -> String {
@@ -99,29 +64,117 @@ extension String {
}
}
struct ReferencedId: Identifiable, Hashable, Equatable {
let ref_id: String
let relay_id: String?
let key: String
enum FollowRef: TagKeys, Hashable, TagConvertible, Equatable {
var id: String {
return ref_id
}
static func q(_ id: String, relay_id: String? = nil) -> ReferencedId {
return ReferencedId(ref_id: id, relay_id: relay_id, key: "q")
}
static func e(_ id: String, relay_id: String? = nil) -> ReferencedId {
return ReferencedId(ref_id: id, relay_id: relay_id, key: "e")
// NOTE: When adding cases make sure to update key and from_tag
case pubkey(Pubkey)
case hashtag(String)
var key: FollowKeys {
switch self {
case .hashtag: return .t
case .pubkey: return .p
}
}
static func p(_ pk: String, relay_id: String? = nil) -> ReferencedId {
return ReferencedId(ref_id: pk, relay_id: relay_id, key: "p")
enum FollowKeys: AsciiCharacter, TagKey, CustomStringConvertible {
case p, t
var keychar: AsciiCharacter { self.rawValue }
var description: String { self.rawValue.description }
}
static func t(_ hashtag: String, relay_id: String? = nil) -> ReferencedId {
return ReferencedId(ref_id: hashtag, relay_id: relay_id, key: "t")
static func from_tag(tag: TagSequence) -> FollowRef? {
guard tag.count >= 2 else { return nil }
var i = tag.makeIterator()
guard let t0 = i.next(),
let c = t0.single_char,
let fkey = FollowKeys(rawValue: c),
let t1 = i.next()
else {
return nil
}
switch fkey {
case .p: return t1.id().map({ .pubkey(Pubkey($0)) })
case .t: return .hashtag(t1.string())
}
}
var tag: [String] {
[key.description, self.description]
}
var description: String {
switch self {
case .pubkey(let pubkey): return pubkey.description
case .hashtag(let string): return string
}
}
}
enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case event(NoteId)
case pubkey(Pubkey)
case quote(QuoteId)
case hashtag(TagElem)
case param(TagElem)
var key: RefKey {
switch self {
case .event: return .e
case .pubkey: return .p
case .quote: return .q
case .hashtag: return .t
case .param: return .d
}
}
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
case e, p, t, d, q
var keychar: AsciiCharacter {
self.rawValue
}
var description: String {
self.keychar.description
}
}
var tag: [String] {
[self.key.description, self.description]
}
var description: String {
switch self {
case .event(let noteId): return noteId.hex()
case .pubkey(let pubkey): return pubkey.hex()
case .quote(let quote): return quote.hex()
case .hashtag(let string): return string.string()
case .param(let string): return string.string()
}
}
static func from_tag(tag: TagSequence) -> RefId? {
var i = tag.makeIterator()
guard tag.count >= 2,
let t0 = i.next(),
let key = t0.single_char,
let rkey = RefKey(rawValue: key),
let t1 = i.next()
else { return nil }
switch rkey {
case .e: return t1.id().map({ .event(NoteId($0)) })
case .p: return t1.id().map({ .pubkey(Pubkey($0)) })
case .q: return t1.id().map({ .quote(QuoteId($0)) })
case .t: return .hashtag(t1)
case .d: return .param(t1)
}
}
}

View File

@@ -200,6 +200,7 @@ final class RelayConnection: ObservableObject {
}
return
}
print("failed to decode event \(messageString)")
case .data(let messageData):
if let messageString = String(data: messageData, encoding: .utf8) {
receive(message: .string(messageString))