1
0
mirror of git://jb55.com/damus synced 2024-09-19 11:43:44 +00:00

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

Changelog-Added: Add support for nostr: bech32 urls in posts and DMs (NIP19)
This commit is contained in:
Bartholomew Joyce 2023-03-30 02:26:54 +02:00 committed by William Casarin
parent a2cd51b6e7
commit c6f4643b5a
4 changed files with 260 additions and 19 deletions

View File

@ -7,10 +7,14 @@
#include "damus.h" #include "damus.h"
#include "bolt11.h" #include "bolt11.h"
#include "bech32.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.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 { struct cursor {
const u8 *p; const u8 *p;
@ -126,7 +130,7 @@ static int parse_str(struct cursor *cur, const char *str) {
return 1; 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; int d1, d2, d3, ind;
const u8 *start = cur->p; const u8 *start = cur->p;
@ -151,8 +155,8 @@ static int parse_mention(struct cursor *cur, struct block *block) {
return 0; return 0;
} }
block->type = BLOCK_MENTION; block->type = BLOCK_MENTION_INDEX;
block->block.mention = ind; block->block.mention_index = ind;
return 1; return 1;
} }
@ -274,6 +278,164 @@ static int parse_invoice(struct cursor *cur, struct block *block) {
return 1; 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) 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)) 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; pre_mention = cur.p;
if (cp == -1 || is_whitespace(cp)) { 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)) if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
return 0; return 0;
continue; 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)) if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
return 0; return 0;
continue; 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) { void blocks_free(struct blocks *blocks) {
if (blocks->blocks) { if (!blocks->blocks) {
free(blocks->blocks); return;
blocks->num_blocks = 0;
} }
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;
} }

View File

@ -9,15 +9,27 @@
#define damus_h #define damus_h
#include <stdio.h> #include <stdio.h>
typedef unsigned char u8;
#define MAX_BLOCKS 1024 #define MAX_BLOCKS 1024
#define MAX_RELAYS 10
enum block_type { enum block_type {
BLOCK_HASHTAG = 1, BLOCK_HASHTAG = 1,
BLOCK_TEXT = 2, BLOCK_TEXT = 2,
BLOCK_MENTION = 3, BLOCK_MENTION_INDEX = 3,
BLOCK_URL = 4, BLOCK_MENTION_BECH32 = 4,
BLOCK_INVOICE = 5, 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 { typedef struct str_block {
@ -32,12 +44,27 @@ typedef struct invoice_block {
}; };
} invoice_block_t; } 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 { typedef struct block {
enum block_type type; enum block_type type;
union { union {
struct str_block str; struct str_block str;
struct invoice_block invoice; struct invoice_block invoice;
int mention; struct mention_bech32_block mention_bech32;
int mention_index;
} block; } block;
} block_t; } block_t;

View File

@ -74,7 +74,9 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
switch block { switch block {
case .mention(let m): case .mention(let m):
if m.type == type { if m.type == type {
acc.insert(m.index) if let idx = m.index {
acc.insert(idx)
}
} }
case .text: case .text:
return return

View File

@ -22,7 +22,7 @@ enum MentionType {
} }
struct Mention { struct Mention {
let index: Int let index: Int?
let type: MentionType let type: MentionType
let ref: ReferencedId let ref: ReferencedId
} }
@ -114,7 +114,13 @@ func render_blocks(blocks: [Block]) -> String {
return blocks.reduce("") { str, block in return blocks.reduce("") { str, block in
switch block { switch block {
case .mention(let m): 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): case .text(let txt):
return str + txt return str + txt
case .hashtag(let htag): case .hashtag(let htag):
@ -177,14 +183,16 @@ func convert_block(_ b: block_t, tags: [[String]]) -> Block? {
return nil return nil
} }
return .text(str) return .text(str)
} else if b.type == BLOCK_MENTION { } else if b.type == BLOCK_MENTION_INDEX {
return convert_mention_block(ind: b.block.mention, tags: tags) return convert_mention_index_block(ind: b.block.mention_index, tags: tags)
} else if b.type == BLOCK_URL { } else if b.type == BLOCK_URL {
return convert_url_block(b.block.str) return convert_url_block(b.block.str)
} else if b.type == BLOCK_INVOICE { } else if b.type == BLOCK_INVOICE {
return convert_invoice_block(b.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 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)) 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? { func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
if let desc = b11.description { if let desc = b11.description {
return .description(String(cString: desc)) return .description(String(cString: desc))
@ -319,7 +356,7 @@ func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
return nil 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) let ind = Int(ind)