diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index e14b3f91..adb08328 100644 --- 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 = ""; }; 4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = ""; }; 4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = ""; }; + 4C4DD3DA2A6CA7E8005B4E85 /* ContentParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentParsing.swift; sourceTree = ""; }; 4C4F14A62A2A61A30045A0B9 /* NostrScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrScriptTests.swift; sourceTree = ""; }; 4C4F14A82A2A71AB0045A0B9 /* nostrscript.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = nostrscript.h; sourceTree = ""; }; 4C4F14A92A2A71AB0045A0B9 /* nostrscript.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = nostrscript.c; sourceTree = ""; }; @@ -1526,6 +1528,7 @@ 4CE6DEEC27F7A08200C66700 /* Preview Content */, 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */, 4C687C262A6039500092C550 /* TestData.swift */, + 4C4DD3DA2A6CA7E8005B4E85 /* ContentParsing.swift */, ); path = damus; sourceTree = ""; @@ -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 new file mode 100644 index 00000000..fa1fa3c0 --- /dev/null +++ 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 index 08bba2ea..5e447d7f 100644 --- 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 { + 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? - + let len = data.withUnsafeMutableBytes { (bytes: UnsafeMutableRawBufferPointer) in return ndb_note_from_json(&json_cstr, Int32(json_cstr.count), ¬e, bytes.baseAddress, Int32(bufsize)) } - + guard let note else { return nil } - + // Create new Data with just the valid bytes let smol_data = Data(bytes: ¬e.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) + } + */ +}