damus

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

commit c6f4643b5abbdfdec617054de0ef6e297d0dba50
parent a2cd51b6e7fbe9810261b6590e9781e58453a265
Author: Bartholomew Joyce <bartholomew.michael.joyce@gmail.com>
Date:   Thu, 30 Mar 2023 02:26:54 +0200

Add support for nostr: bech32 urls in posts and DMs (NIP19)

Changelog-Added: Add support for nostr: bech32 urls in posts and DMs (NIP19)

Diffstat:
Mdamus-c/damus.c | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mdamus-c/damus.h | 35+++++++++++++++++++++++++++++++----
Mdamus/Models/EventRef.swift | 4+++-
Mdamus/Models/Mentions.swift | 49+++++++++++++++++++++++++++++++++++++++++++------
4 files changed, 260 insertions(+), 19 deletions(-)

diff --git a/damus-c/damus.c b/damus-c/damus.c @@ -7,10 +7,14 @@ #include "damus.h" #include "bolt11.h" +#include "bech32.h" #include <stdlib.h> #include <string.h> -typedef unsigned char u8; +#define TLV_SPECIAL 0 +#define TLV_RELAY 1 +#define TLV_AUTHOR 2 +#define TLV_KIND 3 struct cursor { const u8 *p; @@ -126,7 +130,7 @@ static int parse_str(struct cursor *cur, const char *str) { return 1; } -static int parse_mention(struct cursor *cur, struct block *block) { +static int parse_mention_index(struct cursor *cur, struct block *block) { int d1, d2, d3, ind; const u8 *start = cur->p; @@ -151,8 +155,8 @@ static int parse_mention(struct cursor *cur, struct block *block) { return 0; } - block->type = BLOCK_MENTION; - block->block.mention = ind; + block->type = BLOCK_MENTION_INDEX; + block->block.mention_index = ind; return 1; } @@ -274,6 +278,164 @@ static int parse_invoice(struct cursor *cur, struct block *block) { return 1; } +static int parse_mention_bech32(struct cursor *cur, struct block *block) { + const u8 *start, *start_entity, *end; + + start = cur->p; + if (!parse_str(cur, "nostr:")) + return 0; + + start_entity = cur->p; + if (!consume_until_whitespace(cur, 1)) { + cur->p = start; + return 0; + } + + end = cur->p; + + char str[end - start_entity + 1]; + str[end - start_entity] = 0; + memcpy(str, start_entity, end - start_entity); + + char prefix[end - start_entity]; + u8 data[end - start_entity]; + size_t data_len; + size_t max_input_len = end - start_entity + 2; + + if (bech32_decode(prefix, data, &data_len, str, max_input_len) == BECH32_ENCODING_NONE) { + cur->p = start; + return 0; + } + + struct mention_bech32_block mention = { 0 }; + mention.kind = -1; + mention.buffer = (u8*)malloc(data_len); + mention.str.start = (const char*)start; + mention.str.end = (const char*)end; + + size_t len = 0; + if (!bech32_convert_bits(mention.buffer, &len, 8, data, data_len, 5, 0)) { + goto fail; + } + + // Parse type + if (strcmp(prefix, "note") == 0) { + mention.type = NOSTR_BECH32_NOTE; + } else if (strcmp(prefix, "npub") == 0) { + mention.type = NOSTR_BECH32_NPUB; + } else if (strcmp(prefix, "nprofile") == 0) { + mention.type = NOSTR_BECH32_NPROFILE; + } else if (strcmp(prefix, "nevent") == 0) { + mention.type = NOSTR_BECH32_NEVENT; + } else if (strcmp(prefix, "nrelay") == 0) { + mention.type = NOSTR_BECH32_NRELAY; + } else if (strcmp(prefix, "naddr") == 0) { + mention.type = NOSTR_BECH32_NADDR; + } else { + goto fail; + } + + // Parse notes and npubs (non-TLV) + if (mention.type == NOSTR_BECH32_NOTE || mention.type == NOSTR_BECH32_NPUB) { + if (len != 32) goto fail; + if (mention.type == NOSTR_BECH32_NOTE) { + mention.event_id = mention.buffer; + } else { + mention.pubkey = mention.buffer; + } + goto ok; + } + + // Parse TLV entities + const int MAX_VALUES = 16; + int values_count = 0; + u8 Ts[MAX_VALUES]; + u8 Ls[MAX_VALUES]; + u8* Vs[MAX_VALUES]; + for (int i = 0; i < len - 1;) { + if (values_count == MAX_VALUES) goto fail; + + Ts[values_count] = mention.buffer[i++]; + Ls[values_count] = mention.buffer[i++]; + if (Ls[values_count] > len - i) goto fail; + + Vs[values_count] = &mention.buffer[i]; + i += Ls[values_count]; + ++values_count; + } + + // Decode and validate all TLV-type entities + if (mention.type == NOSTR_BECH32_NPROFILE) { + for (int i = 0; i < values_count; ++i) { + if (Ts[i] == TLV_SPECIAL) { + if (Ls[i] != 32 || mention.pubkey) goto fail; + mention.pubkey = Vs[i]; + } else if (Ts[i] == TLV_RELAY) { + if (mention.relays_count == MAX_RELAYS) goto fail; + Vs[i][Ls[i]] = 0; + mention.relays[mention.relays_count++] = (char*)Vs[i]; + } else { + goto fail; + } + } + if (!mention.pubkey) goto fail; + + } else if (mention.type == NOSTR_BECH32_NEVENT) { + for (int i = 0; i < values_count; ++i) { + if (Ts[i] == TLV_SPECIAL) { + if (Ls[i] != 32 || mention.event_id) goto fail; + mention.event_id = Vs[i]; + } else if (Ts[i] == TLV_RELAY) { + if (mention.relays_count == MAX_RELAYS) goto fail; + Vs[i][Ls[i]] = 0; + mention.relays[mention.relays_count++] = (char*)Vs[i]; + } else if (Ts[i] == TLV_AUTHOR) { + if (Ls[i] != 32 || mention.pubkey) goto fail; + mention.pubkey = Vs[i]; + } else { + goto fail; + } + } + if (!mention.event_id) goto fail; + + } else if (mention.type == NOSTR_BECH32_NRELAY) { + if (values_count != 1 || Ts[0] != TLV_SPECIAL) goto fail; + Vs[0][Ls[0]] = 0; + mention.relays[mention.relays_count++] = (char*)Vs[0]; + + } else { // entity.type == NOSTR_BECH32_NADDR + for (int i = 0; i < values_count; ++i) { + if (Ts[i] == TLV_SPECIAL) { + Vs[i][Ls[i]] = 0; + mention.identifier = (char*)Vs[i]; + } else if (Ts[i] == TLV_RELAY) { + if (mention.relays_count == MAX_RELAYS) goto fail; + Vs[i][Ls[i]] = 0; + mention.relays[mention.relays_count++] = (char*)Vs[i]; + } else if (Ts[i] == TLV_AUTHOR) { + if (Ls[i] != 32 || mention.pubkey) goto fail; + mention.pubkey = Vs[i]; + } else if (Ts[i] == TLV_KIND) { + if (Ls[i] != sizeof(int) || mention.kind != -1) goto fail; + mention.kind = *(int*)Vs[i]; + } else { + goto fail; + } + } + if (!mention.identifier || mention.kind == -1 || !mention.pubkey) goto fail; + } + +ok: + block->type = BLOCK_MENTION_BECH32; + block->block.mention_bech32 = mention; + return 1; + +fail: + free(mention.buffer); + cur->p = start; + return 0; +} + static int add_text_then_block(struct cursor *cur, struct blocks *blocks, struct block block, const u8 **start, const u8 *pre_mention) { if (!add_text_block(blocks, *start, pre_mention)) @@ -303,7 +465,7 @@ int damus_parse_content(struct blocks *blocks, const char *content) { pre_mention = cur.p; if (cp == -1 || is_whitespace(cp)) { - if (c == '#' && (parse_mention(&cur, &block) || parse_hashtag(&cur, &block))) { + if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) { if (!add_text_then_block(&cur, blocks, block, &start, pre_mention)) return 0; continue; @@ -315,6 +477,10 @@ int damus_parse_content(struct blocks *blocks, const char *content) { if (!add_text_then_block(&cur, blocks, block, &start, pre_mention)) return 0; continue; + } else if (c == 'n' && parse_mention_bech32(&cur, &block)) { + if (!add_text_then_block(&cur, blocks, block, &start, pre_mention)) + return 0; + continue; } } @@ -335,8 +501,17 @@ void blocks_init(struct blocks *blocks) { } void blocks_free(struct blocks *blocks) { - if (blocks->blocks) { - free(blocks->blocks); - blocks->num_blocks = 0; + if (!blocks->blocks) { + return; + } + + for (int i = 0; i < blocks->num_blocks; ++i) { + if (blocks->blocks[i].type == BLOCK_MENTION_BECH32) { + free(blocks->blocks[i].block.mention_bech32.buffer); + blocks->blocks[i].block.mention_bech32.buffer = NULL; + } } + + free(blocks->blocks); + blocks->num_blocks = 0; } diff --git a/damus-c/damus.h b/damus-c/damus.h @@ -9,15 +9,27 @@ #define damus_h #include <stdio.h> +typedef unsigned char u8; #define MAX_BLOCKS 1024 +#define MAX_RELAYS 10 enum block_type { BLOCK_HASHTAG = 1, BLOCK_TEXT = 2, - BLOCK_MENTION = 3, - BLOCK_URL = 4, - BLOCK_INVOICE = 5, + BLOCK_MENTION_INDEX = 3, + BLOCK_MENTION_BECH32 = 4, + BLOCK_URL = 5, + BLOCK_INVOICE = 6, +}; + +enum nostr_bech32_type { + NOSTR_BECH32_NOTE = 1, + NOSTR_BECH32_NPUB = 2, + NOSTR_BECH32_NPROFILE = 3, + NOSTR_BECH32_NEVENT = 4, + NOSTR_BECH32_NRELAY = 5, + NOSTR_BECH32_NADDR = 6, }; typedef struct str_block { @@ -32,12 +44,27 @@ typedef struct invoice_block { }; } invoice_block_t; +typedef struct mention_bech32_block { + struct str_block str; + enum nostr_bech32_type type; + + u8 *event_id; + u8 *pubkey; + char *identifier; + char *relays[MAX_RELAYS]; + int relays_count; + int kind; + + u8* buffer; +} mention_bech32_block_t; + typedef struct block { enum block_type type; union { struct str_block str; struct invoice_block invoice; - int mention; + struct mention_bech32_block mention_bech32; + int mention_index; } block; } block_t; diff --git a/damus/Models/EventRef.swift b/damus/Models/EventRef.swift @@ -74,7 +74,9 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> { switch block { case .mention(let m): if m.type == type { - acc.insert(m.index) + if let idx = m.index { + acc.insert(idx) + } } case .text: return diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift @@ -22,7 +22,7 @@ enum MentionType { } struct Mention { - let index: Int + let index: Int? let type: MentionType let ref: ReferencedId } @@ -114,7 +114,13 @@ func render_blocks(blocks: [Block]) -> String { return blocks.reduce("") { str, block in switch block { case .mention(let m): - return str + "#[\(m.index)]" + if let idx = m.index { + return str + "#[\(idx)]" + } else if m.type == .pubkey { + return str + "nostr:\(bech32_pubkey(m.ref.ref_id)!)" + } else { + return str + "nostr:\(bech32_note_id(m.ref.ref_id)!)" + } case .text(let txt): return str + txt case .hashtag(let htag): @@ -177,14 +183,16 @@ func convert_block(_ b: block_t, tags: [[String]]) -> Block? { 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_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 } @@ -307,6 +315,35 @@ func convert_invoice_block(_ b: invoice_block) -> Block? { 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? +{ + let relay_id = b.relays_count > 0 ? String(cString: b.relays.0!) : nil + + switch b.type { + case NOSTR_BECH32_NOTE: + fallthrough + case NOSTR_BECH32_NEVENT: + let event_id = hex_encode(Data(bytes: b.event_id, count: 32)) + 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: + fallthrough + case NOSTR_BECH32_NPROFILE: + let pubkey = hex_encode(Data(bytes: b.pubkey, count: 32)) + 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: + fallthrough + case NOSTR_BECH32_NADDR: + return .text(strblock_to_string(b.str)!) + + default: + return nil + } +} + func convert_invoice_description(b11: bolt11) -> InvoiceDescription? { if let desc = b11.description { return .description(String(cString: desc)) @@ -319,7 +356,7 @@ func convert_invoice_description(b11: bolt11) -> InvoiceDescription? { return nil } -func convert_mention_block(ind: Int32, tags: [[String]]) -> Block? +func convert_mention_index_block(ind: Int32, tags: [[String]]) -> Block? { let ind = Int(ind)