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:
116
damus/Nostr/Id.swift
Normal file
116
damus/Nostr/Id.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user