damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 7ec8da6c73b8ed7203678bb7770a52126d85a0f7
parent 9e659c49b554a3775af02e55d36f736078ed0343
Author: William Casarin <jb55@jb55.com>
Date:   Sat, 22 Jul 2023 17:15:36 -0700

ndb: start implementing existing NostrEvent functionality

We eventually want to switch over to NdbNote instead of NostrEvent. To
facilitate this, the plan is to eventually make NostrEvent an alias of
NdbNote. For this to work, let's make sure the NostrEvent extensions are
implemented on NdbNote.

We will likely switch away from string properties as well, but for now
we will try to emulate as much as possible to make sure everything is
working first.

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Adamus/ContentParsing.swift | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnostrdb/NdbNote.swift | 241+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
3 files changed, 309 insertions(+), 11 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -128,6 +128,7 @@ 4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */; }; 4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; }; 4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; }; + 4C4DD3DB2A6CA7E8005B4E85 /* ContentParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4DD3DA2A6CA7E8005B4E85 /* ContentParsing.swift */; }; 4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4F14A62A2A61A30045A0B9 /* NostrScriptTests.swift */; }; 4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */; }; 4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0929A55429003E4487 /* EventGroup.swift */; }; @@ -609,6 +610,7 @@ 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = "<group>"; }; 4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; }; 4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; }; + 4C4DD3DA2A6CA7E8005B4E85 /* ContentParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentParsing.swift; sourceTree = "<group>"; }; 4C4F14A62A2A61A30045A0B9 /* NostrScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrScriptTests.swift; sourceTree = "<group>"; }; 4C4F14A82A2A71AB0045A0B9 /* nostrscript.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = nostrscript.h; sourceTree = "<group>"; }; 4C4F14A92A2A71AB0045A0B9 /* nostrscript.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = nostrscript.c; sourceTree = "<group>"; }; @@ -1526,6 +1528,7 @@ 4CE6DEEC27F7A08200C66700 /* Preview Content */, 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */, 4C687C262A6039500092C550 /* TestData.swift */, + 4C4DD3DA2A6CA7E8005B4E85 /* ContentParsing.swift */, ); path = damus; sourceTree = "<group>"; @@ -1955,6 +1958,7 @@ 4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */, F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */, 4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */, + 4C4DD3DB2A6CA7E8005B4E85 /* ContentParsing.swift in Sources */, 4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */, 4C7D09602A098C5D00943473 /* WalletView.swift in Sources */, 4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */, diff --git a/damus/ContentParsing.swift b/damus/ContentParsing.swift @@ -0,0 +1,75 @@ +// +// ContentParsing.swift +// damus +// +// Created by William Casarin on 2023-07-22. +// + +import Foundation + +func tag_to_refid_ndb(_ tag: TagSequence) -> ReferencedId? { + guard let ref_id = tag[1]?.string(), + let key = tag[0]?.string() else { return nil } + + let relay_id = tag[2]?.string() + + return ReferencedId(ref_id: ref_id, relay_id: relay_id, key: key) +} + +func convert_mention_index_block_ndb(ind: Int, tags: TagsSequence) -> Block? { + if ind < 0 || (ind + 1 > tags.count) || tags[ind]!.count < 2 { + return .text("#[\(ind)]") + } + + guard let tag = tags[ind], let fst = tag.first(where: { _ in true }) else { + return nil + } + + guard let mention_type = parse_mention_type_ndb(fst) else { + return .text("#[\(ind)]") + } + + guard let ref = tag_to_refid_ndb(tag) else { + return .text("#[\(ind)]") + } + + return .mention(Mention(index: ind, type: mention_type, ref: ref)) +} + + +func convert_block_ndb(_ b: block_t, tags: TagsSequence) -> Block? { + if b.type == BLOCK_MENTION_INDEX { + return convert_mention_index_block_ndb(ind: Int(b.block.mention_index), tags: tags) + } + + return convert_block(b, tags: []) +} + + +func parse_note_content_ndb(note: NdbNote) -> Blocks { + var out: [Block] = [] + + var bs = note_blocks() + bs.num_blocks = 0; + + blocks_init(&bs) + + damus_parse_content(&bs, note.content_raw) + + var i = 0 + while (i < bs.num_blocks) { + let block = bs.blocks[i] + + if let converted = convert_block_ndb(block, tags: note.tags()) { + out.append(converted) + } + + i += 1 + } + + let words = Int(bs.words) + blocks_free(&bs) + + return Blocks(words: words, blocks: out) +} + diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift @@ -16,40 +16,259 @@ struct NdbNote { self.note = note self.owned = data } - + var owned_size: Int? { return owned?.count } - + var content: String { - String(cString: ndb_note_content(note), encoding: .utf8) ?? "" + String(cString: content_raw, encoding: .utf8) ?? "" + } + + var content_raw: UnsafePointer<CChar> { + ndb_note_content(note) + } + + var content_len: UInt32 { + ndb_note_content_length(note) } var id: Data { Data(buffer: UnsafeBufferPointer(start: ndb_note_id(note), count: 32)) } - + var pubkey: Data { Data(buffer: UnsafeBufferPointer(start: ndb_note_pubkey(note), count: 32)) } - + + var created_at: UInt32 { + ndb_note_created_at(note) + } + + var kind: UInt32 { + ndb_note_kind(note) + } + func tags() -> TagsSequence { return .init(note: self) } - + static func owned_from_json(json: String, bufsize: Int = 2 << 18) -> NdbNote? { var data = Data(capacity: bufsize) guard var json_cstr = json.cString(using: .utf8) else { return nil } - + var note: UnsafeMutablePointer<ndb_note>? - + let len = data.withUnsafeMutableBytes { (bytes: UnsafeMutableRawBufferPointer) in return ndb_note_from_json(&json_cstr, Int32(json_cstr.count), &note, bytes.baseAddress, Int32(bufsize)) } - + guard let note else { return nil } - + // Create new Data with just the valid bytes let smol_data = Data(bytes: &note.pointee, count: Int(len)) return NdbNote(note: note, data: smol_data) - }} + } +} + + +// NostrEvent compat +extension NdbNote { + var is_textlike: Bool { + return kind == 1 || kind == 42 || kind == 30023 + } + + var known_kind: NostrKind? { + return NostrKind.init(rawValue: Int(kind)) + } + + var too_big: Bool { + return known_kind != .longform && self.content_len > 16000 + } + + var should_show_event: Bool { + return !too_big + } + + + //var is_valid_id: Bool { + // return calculate_event_id(ev: self) == self.id + //} + + func get_blocks(content: String) -> Blocks { + return parse_note_content_ndb(note: self) + } + + /* + + func get_inner_event(cache: EventCache) -> NostrEvent? { + guard self.known_kind == .boost else { + return nil + } + + if self.content == "", let ref = self.referenced_ids.first { + return cache.lookup(ref.ref_id) + } + + return self.inner_event + } + + func event_refs(_ privkey: String?) -> [EventRef] { + if let rs = _event_refs { + return rs + } + let refs = interpret_event_refs(blocks: self.blocks(privkey).blocks, tags: self.tags) + self._event_refs = refs + return refs + } + + + func decrypted(privkey: String?) -> String? { + if let decrypted_content = decrypted_content { + return decrypted_content + } + + guard let key = privkey else { + return nil + } + + guard let our_pubkey = privkey_to_pubkey(privkey: key) else { + return nil + } + + var pubkey = self.pubkey + // This is our DM, we need to use the pubkey of the person we're talking to instead + if our_pubkey == pubkey { + guard let refkey = self.referenced_pubkeys.first else { + return nil + } + + pubkey = refkey.ref_id + } + + let dec = decrypt_dm(key, pubkey: pubkey, content: self.content, encoding: .base64) + self.decrypted_content = dec + + return dec + } + + func get_content(_ privkey: String?) -> String { + if known_kind == .dm { + return decrypted(privkey: privkey) ?? "*failed to decrypt content*" + } + + return content + } + + var description: String { + return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) content '\(content)' }" + } + + var known_kind: NostrKind? { + 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) + } + + public func direct_replies(_ privkey: String?) -> [ReferencedId] { + return event_refs(privkey).reduce(into: []) { acc, evref in + if let direct_reply = evref.is_direct_reply { + acc.append(direct_reply) + } + } + } + + public func thread_id(privkey: String?) -> String { + for ref in event_refs(privkey) { + if let thread_id = ref.is_thread_id { + return thread_id.ref_id + } + } + + return self.id + } + + public func last_refid() -> ReferencedId? { + var mlast: Int? = nil + var i: Int = 0 + for tag in tags { + if tag.count >= 2 && tag[0] == "e" { + mlast = i + } + i += 1 + } + + guard let last = mlast else { + return nil + } + + return tag_to_refid(tags[last]) + } + + public func references(id: String, key: String) -> Bool { + for tag in tags { + if tag.count >= 2 && tag[0] == key { + if tag[1] == id { + return true + } + } + } + + return false + } + + func is_reply(_ privkey: String?) -> Bool { + return event_is_reply(self, privkey: privkey) + } + + func note_language(_ privkey: String?) -> String? { + // Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in + // and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer. + let originalBlocks = blocks(privkey).blocks + let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ") + + // Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate. + let languageRecognizer = NLLanguageRecognizer() + languageRecognizer.processString(originalOnlyText) + + guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue else { + return nil + } + + // Remove the variant component and just take the language part as translation services typically only supports the variant-less language. + // Moreover, speakers of one variant can generally understand other variants. + return localeToLanguage(locale) + } + + public var referenced_ids: [ReferencedId] { + return get_referenced_ids(key: "e") + } + + public var referenced_pubkeys: [ReferencedId] { + return get_referenced_ids(key: "p") + } + + public var is_local: Bool { + return (self.flags & 1) != 0 + } + + func calculate_id() { + self.id = calculate_event_id(ev: self) + } + + func sign(privkey: String) { + self.sig = sign_event(privkey: privkey, ev: self) + } + + var age: TimeInterval { + let event_date = Date(timeIntervalSince1970: TimeInterval(created_at)) + return Date.now.timeIntervalSince(event_date) + } + */ +}