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:
parent
a2cd51b6e7
commit
c6f4643b5a
191
damus-c/damus.c
191
damus-c/damus.c
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user