Files
damus/damus/Models/Mentions.swift
William Casarin db2ec0a00a Fix npub mention bugs, fix slowness when parsing large posts
Switch the post parser to use the same code as the content parser. This
was causing many issues, including performance issues.

Changelog-Fixed: Fix lag when creating large posts
Changelog-Fixed: Fix npub mentions failing to parse in some cases
Changelog-Added: Add r tag when mentioning a url
Changelog-Removed: Remove old @ and & hex key mentions
2023-07-11 09:15:13 -07:00

479 lines
13 KiB
Swift

//
// Mentions.swift
// damus
//
// Created by William Casarin on 2022-05-04.
//
import Foundation
enum MentionType {
case pubkey
case event
var ref: String {
switch self {
case .pubkey:
return "p"
case .event:
return "e"
}
}
}
struct Mention: Equatable {
let index: Int?
let type: MentionType
let ref: ReferencedId
static func note(_ id: String) -> Mention {
return Mention(index: nil, type: .event, ref: .e(id))
}
static func pubkey(_ pubkey: String) -> Mention {
return Mention(index: nil, type: .pubkey, ref: .p(pubkey))
}
}
typealias Invoice = LightningInvoice<Amount>
typealias ZapInvoice = LightningInvoice<Int64>
enum InvoiceDescription {
case description(String)
case description_hash(Data)
}
struct LightningInvoice<T> {
let description: InvoiceDescription
let amount: T
let string: String
let expiry: UInt64
let payment_hash: Data
let created_at: UInt64
var description_string: String {
switch description {
case .description(let string):
return string
case .description_hash:
return ""
}
}
}
enum Block: Equatable {
static func == (lhs: Block, rhs: Block) -> Bool {
switch (lhs, rhs) {
case (.text(let a), .text(let b)):
return a == b
case (.mention(let a), .mention(let b)):
return a == b
case (.hashtag(let a), .hashtag(let b)):
return a == b
case (.url(let a), .url(let b)):
return a == b
case (.invoice(let a), .invoice(let b)):
return a.string == b.string
case (_, _):
return false
}
}
case text(String)
case mention(Mention)
case hashtag(String)
case url(URL)
case invoice(Invoice)
case relay(String)
var is_invoice: Invoice? {
if case .invoice(let invoice) = self {
return invoice
}
return nil
}
var is_hashtag: String? {
if case .hashtag(let htag) = self {
return htag
}
return nil
}
var is_url: URL? {
if case .url(let url) = self {
return url
}
return nil
}
var is_text: String? {
if case .text(let txt) = self {
return txt
}
return nil
}
var is_note_mention: Bool {
guard case .mention(let mention) = self else {
return false
}
return mention.type == .event
}
var is_mention: Mention? {
if case .mention(let m) = self {
return m
}
return nil
}
}
func render_blocks(blocks: [Block]) -> String {
return blocks.reduce("") { str, block in
switch block {
case .mention(let m):
if let idx = m.index {
return str + "#[\(idx)]"
} else if m.type == .pubkey, let pk = bech32_pubkey(m.ref.ref_id) {
return str + "nostr:\(pk)"
} else if let note_id = bech32_note_id(m.ref.ref_id) {
return str + "nostr:\(note_id)"
} else {
return str + m.ref.ref_id
}
case .relay(let relay):
return str + relay
case .text(let txt):
return str + txt
case .hashtag(let htag):
return str + "#" + htag
case .url(let url):
return str + url.absoluteString
case .invoice(let inv):
return str + inv.string
}
}
}
struct Blocks {
let words: Int
let blocks: [Block]
}
func parse_mentions(content: String, tags: [[String]]) -> Blocks {
var out: [Block] = []
var bs = note_blocks()
bs.num_blocks = 0;
blocks_init(&bs)
let bytes = content.utf8CString
let _ = bytes.withUnsafeBufferPointer { p in
damus_parse_content(&bs, p.baseAddress)
}
var i = 0
while (i < bs.num_blocks) {
let block = bs.blocks[i]
if let converted = convert_block(block, tags: tags) {
out.append(converted)
}
i += 1
}
let words = Int(bs.words)
blocks_free(&bs)
return Blocks(words: words, blocks: out)
}
func strblock_to_string(_ s: str_block_t) -> String? {
let len = s.end - s.start
let bytes = Data(bytes: s.start, count: len)
return String(bytes: bytes, encoding: .utf8)
}
func convert_block(_ b: block_t, tags: [[String]]) -> Block? {
if b.type == BLOCK_HASHTAG {
guard let str = strblock_to_string(b.block.str) else {
return nil
}
return .hashtag(str)
} else if b.type == BLOCK_TEXT {
guard let str = strblock_to_string(b.block.str) else {
return nil
}
return .text(str)
} else if b.type == BLOCK_MENTION_INDEX {
return convert_mention_index_block(ind: b.block.mention_index, tags: tags)
} else if b.type == BLOCK_URL {
return convert_url_block(b.block.str)
} else if b.type == BLOCK_INVOICE {
return convert_invoice_block(b.block.invoice)
} else if b.type == BLOCK_MENTION_BECH32 {
return convert_mention_bech32_block(b.block.mention_bech32)
}
return nil
}
func convert_url_block(_ b: str_block) -> Block? {
guard let str = strblock_to_string(b) else {
return nil
}
guard let url = URL(string: str) else {
return .text(str)
}
return .url(url)
}
func maybe_pointee<T>(_ p: UnsafeMutablePointer<T>!) -> T? {
guard p != nil else {
return nil
}
return p.pointee
}
enum Amount: Equatable {
case any
case specific(Int64)
func amount_sats_str() -> String {
switch self {
case .any:
return NSLocalizedString("Any", comment: "Any amount of sats")
case .specific(let amt):
return format_msats(amt)
}
}
}
func format_msats_abbrev(_ msats: Int64) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.positiveSuffix = "m"
formatter.positivePrefix = ""
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 3
formatter.roundingMode = .down
formatter.roundingIncrement = 0.1
formatter.multiplier = 1
let sats = NSNumber(value: (Double(msats) / 1000.0))
if msats >= 1_000_000*1000 {
formatter.positiveSuffix = "m"
formatter.multiplier = 0.000001
} else if msats >= 1000*1000 {
formatter.positiveSuffix = "k"
formatter.multiplier = 0.001
} else {
return sats.stringValue
}
return formatter.string(from: sats) ?? sats.stringValue
}
func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
numberFormatter.locale = locale
let sats = NSNumber(value: (Double(msat) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
let format = localizedStringFormat(key: "sats_count", locale: locale)
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
}
func convert_invoice_block(_ b: invoice_block) -> Block? {
guard let invstr = strblock_to_string(b.invstr) else {
return nil
}
guard var b11 = maybe_pointee(b.bolt11) else {
return nil
}
guard let description = convert_invoice_description(b11: b11) else {
return nil
}
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
let payment_hash = Data(bytes: &b11.payment_hash, count: 32)
let created_at = b11.timestamp
tal_free(b.bolt11)
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
}
func convert_mention_bech32_block(_ b: mention_bech32_block) -> Block?
{
switch b.bech32.type {
case NOSTR_BECH32_NOTE:
let note = b.bech32.data.note;
let event_id = hex_encode(Data(bytes: note.event_id, count: 32))
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: nil, key: "e")
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
case NOSTR_BECH32_NEVENT:
let nevent = b.bech32.data.nevent;
let event_id = hex_encode(Data(bytes: nevent.event_id, count: 32))
var relay_id: String? = nil
if nevent.relays.num_relays > 0 {
relay_id = strblock_to_string(nevent.relays.relays.0)
}
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: relay_id, key: "e")
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
case NOSTR_BECH32_NPUB:
let npub = b.bech32.data.npub
let pubkey = hex_encode(Data(bytes: npub.pubkey, count: 32))
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
case NOSTR_BECH32_NSEC:
let nsec = b.bech32.data.nsec
let nsec_bytes = Data(bytes: nsec.nsec, count: 32)
let pubkey = privkey_to_pubkey_raw(sec: nsec_bytes.bytes) ?? hex_encode(nsec_bytes)
return .mention(.pubkey(pubkey))
case NOSTR_BECH32_NPROFILE:
let nprofile = b.bech32.data.nprofile
let pubkey = hex_encode(Data(bytes: nprofile.pubkey, count: 32))
var relay_id: String? = nil
if nprofile.relays.num_relays > 0 {
relay_id = strblock_to_string(nprofile.relays.relays.0)
}
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: relay_id, key: "p")
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
case NOSTR_BECH32_NRELAY:
let nrelay = b.bech32.data.nrelay
guard let relay_str = strblock_to_string(nrelay.relay) else {
return nil
}
return .relay(relay_str)
case NOSTR_BECH32_NADDR:
// TODO: wtf do I do with this
guard let naddr = strblock_to_string(b.str) else {
return nil
}
return .text("nostr:" + naddr)
default:
return nil
}
}
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
if let desc = b11.description {
return .description(String(cString: desc))
}
if var deschash = maybe_pointee(b11.description_hash) {
return .description_hash(Data(bytes: &deschash, count: 32))
}
return nil
}
func convert_mention_index_block(ind: Int32, tags: [[String]]) -> Block?
{
let ind = Int(ind)
if ind < 0 || (ind + 1 > tags.count) || tags[ind].count < 2 {
return .text("#[\(ind)]")
}
let tag = tags[ind]
guard let mention_type = parse_mention_type(tag[0]) else {
return .text("#[\(ind)]")
}
guard let ref = tag_to_refid(tag) else {
return .text("#[\(ind)]")
}
return .mention(Mention(index: ind, type: mention_type, ref: ref))
}
func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? {
var i: Int = 0
for tag in tags {
if tag.count >= 2 {
if tag[0] == type && tag[1] == id {
return i
}
}
i += 1
}
return nil
}
struct PostTags {
let blocks: [Block]
let tags: [[String]]
}
func parse_mention_type(_ c: String) -> MentionType? {
if c == "e" {
return .event
} else if c == "p" {
return .pubkey
}
return nil
}
/// Convert
func make_post_tags(post_blocks: [Block], tags: [[String]], silent_mentions: Bool) -> PostTags {
var new_tags = tags
for post_block in post_blocks {
switch post_block {
case .mention(let mention):
let mention_type = mention.type
if silent_mentions || mention_type == .event {
continue
}
new_tags.append(refid_to_tag(mention.ref))
case .hashtag(let hashtag):
new_tags.append(["t", hashtag.lowercased()])
case .text: break
case .invoice: break
case .relay: break
case .url(let url):
new_tags.append(["r", url.absoluteString])
break
}
}
return PostTags(blocks: post_blocks, tags: new_tags)
}
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
let tags = post.references.map(refid_to_tag) + post.tags
let post_blocks = parse_post_blocks(content: post.content)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: false)
let content = render_blocks(blocks: post_tags.blocks)
let new_ev = NostrEvent(content: content, pubkey: pubkey, kind: post.kind.rawValue, tags: post_tags.tags)
new_ev.calculate_id()
new_ev.sign(privkey: privkey)
return new_ev
}