Files
damus/damus/Models/Mentions.swift
William Casarin 5fdcecd44f Fix crash in null bolt11 descriptions
Changelog-Fixed: Fixed crashed on lightning invoices with empty descriptions
2022-12-18 09:23:16 -08:00

490 lines
12 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 {
let index: Int
let type: MentionType
let ref: ReferencedId
}
struct IdBlock: Identifiable {
let id: String = UUID().description
let block: Block
}
struct Invoice {
let description: String
let amount: Int64
let string: String
let expiry: UInt64
let payment_hash: Data
let created_at: UInt64
}
enum Block {
case text(String)
case mention(Mention)
case hashtag(String)
case url(URL)
case invoice(Invoice)
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_mention: Bool {
if case .mention = self {
return true
}
return false
}
}
func render_blocks(blocks: [Block]) -> String {
return blocks.reduce("") { str, block in
switch block {
case .mention(let m):
return str + "#[\(m.index)]"
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
}
}
}
func parse_textblock(str: String, from: Int, to: Int) -> Block {
return .text(String(substring(str, start: from, end: to)))
}
func parse_mentions(content: String, tags: [[String]]) -> [Block] {
var out: [Block] = []
var bs = blocks()
bs.num_blocks = 0;
blocks_init(&bs)
let bytes = content.utf8CString
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
}
blocks_free(&bs)
return 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 {
return convert_mention_block(ind: b.block.mention, 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)
}
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
}
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
}
var description = ""
if b11.description != nil {
description = String(cString: b11.description)
}
guard let msat = maybe_pointee(b11.msat) else {
return nil
}
let amount = Int64(msat.millisatoshis)
let payment_hash = Data(bytes: &b11.payment_hash, count: 32)
let hex = hex_encode(payment_hash)
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_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 parse_mentions_old(content: String, tags: [[String]]) -> [Block] {
let p = Parser(pos: 0, str: content)
var blocks: [Block] = []
var starting_from: Int = 0
while p.pos < content.count {
if !consume_until(p, match: { !$0.isWhitespace}) {
break
}
let pre_mention = p.pos
let c = peek_char(p, 0)
let pr = peek_char(p, -1)
if c == "#" {
if let mention = parse_mention(p, tags: tags) {
blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention))
blocks.append(.mention(mention))
starting_from = p.pos
} else if let hashtag = parse_hashtag(p) {
blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention))
blocks.append(.hashtag(hashtag))
starting_from = p.pos
} else {
if !consume_until(p, match: { $0.isWhitespace }) {
break
}
}
} else if c == "h" && (pr == nil || pr!.isWhitespace) {
if let url = parse_url(p) {
blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention))
blocks.append(.url(url))
starting_from = p.pos
} else {
if !consume_until(p, match: { $0.isWhitespace }) {
break
}
}
} else {
if !consume_until(p, match: { $0.isWhitespace }) {
break
}
}
}
if p.str.count - starting_from > 0 {
blocks.append(parse_textblock(str: p.str, from: starting_from, to: p.str.count))
}
return blocks
}
func parse_while(_ p: Parser, match: (Character) -> Bool) -> String? {
var i: Int = 0
let sub = substring(p.str, start: p.pos, end: p.str.count)
let start = p.pos
for c in sub {
if match(c) {
p.pos += 1
} else {
break
}
i += 1
}
let end = start + i
if start == end {
return nil
}
return String(substring(p.str, start: start, end: end))
}
func is_hashtag_char(_ c: Character) -> Bool {
return c.isLetter || c.isNumber
}
func prev_char(_ p: Parser, n: Int) -> Character? {
if p.pos - n < 0 {
return nil
}
let ind = p.str.index(p.str.startIndex, offsetBy: p.pos - n)
return p.str[ind]
}
func is_punctuation(_ c: Character) -> Bool {
return c.isWhitespace || c.isPunctuation
}
func parse_url(_ p: Parser) -> URL? {
let start = p.pos
if !parse_str(p, "http") {
return nil
}
if parse_char(p, "s") {
if !parse_str(p, "://") {
return nil
}
} else {
if !parse_str(p, "://") {
return nil
}
}
if !consume_until(p, match: { c in c.isWhitespace }, end_ok: true) {
p.pos = start
return nil
}
let url_str = String(substring(p.str, start: start, end: p.pos))
guard let url = URL(string: url_str) else {
p.pos = start
return nil
}
return url
}
func parse_hashtag(_ p: Parser) -> String? {
let start = p.pos
if !parse_char(p, "#") {
return nil
}
if let prev = prev_char(p, n: 2) {
// we don't allow adjacent hashtags
if !is_punctuation(prev) {
return nil
}
}
guard let str = parse_while(p, match: is_hashtag_char) else {
p.pos = start
return nil
}
return str
}
func parse_mention(_ p: Parser, tags: [[String]]) -> Mention? {
let start = p.pos
if !parse_str(p, "#[") {
return nil
}
guard let digit = parse_digit(p) else {
p.pos = start
return nil
}
var ind = digit
if let d2 = parse_digit(p) {
ind = digit * 10
ind += d2
}
if !parse_char(p, "]") {
return nil
}
var kind: MentionType = .pubkey
if ind > tags.count - 1 {
return nil
}
if tags[ind].count == 0 {
return nil
}
switch tags[ind][0] {
case "e": kind = .event
case "p": kind = .pubkey
default: return nil
}
guard let ref = tag_to_refid(tags[ind]) else {
return nil
}
return Mention(index: ind, type: kind, 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: [PostBlock], tags: [[String]]) -> PostTags {
var new_tags = tags
var blocks: [Block] = []
for post_block in post_blocks {
switch post_block {
case .ref(let ref):
guard let mention_type = parse_mention_type(ref.key) else {
continue
}
if let ind = find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) {
let mention = Mention(index: ind, type: mention_type, ref: ref)
let block = Block.mention(mention)
blocks.append(block)
} else {
let ind = new_tags.count
new_tags.append(refid_to_tag(ref))
let mention = Mention(index: ind, type: mention_type, ref: ref)
let block = Block.mention(mention)
blocks.append(block)
}
case .hashtag(let hashtag):
new_tags.append(["t", hashtag.lowercased()])
blocks.append(.hashtag(hashtag))
case .text(let txt):
blocks.append(Block.text(txt))
}
}
return PostTags(blocks: blocks, tags: new_tags)
}
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
let tags = post.references.map(refid_to_tag)
let post_blocks = parse_post_blocks(content: post.content)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
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
}