damus

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

damus.c (9006B)


      1 //
      2 //  damus.c
      3 //  damus
      4 //
      5 //  Created by William Casarin on 2022-10-17.
      6 //
      7 
      8 #include "damus.h"
      9 #include "cursor.h"
     10 #include "bolt11.h"
     11 #include "bech32.h"
     12 #include <stdlib.h>
     13 #include <string.h>
     14 
     15 static int parse_digit(struct cursor *cur, int *digit) {
     16     int c;
     17     if ((c = peek_char(cur, 0)) == -1)
     18         return 0;
     19     
     20     c -= '0';
     21     
     22     if (c >= 0 && c <= 9) {
     23         *digit = c;
     24         cur->p++;
     25         return 1;
     26     }
     27     return 0;
     28 }
     29 
     30 
     31 static int parse_mention_index(struct cursor *cur, struct note_block *block) {
     32     int d1, d2, d3, ind;
     33     u8 *start = cur->p;
     34     
     35     if (!parse_str(cur, "#["))
     36         return 0;
     37     
     38     if (!parse_digit(cur, &d1)) {
     39         cur->p = start;
     40         return 0;
     41     }
     42     
     43     ind = d1;
     44     
     45     if (parse_digit(cur, &d2))
     46         ind = (d1 * 10) + d2;
     47     
     48     if (parse_digit(cur, &d3))
     49         ind = (d1 * 100) + (d2 * 10) + d3;
     50     
     51     if (!parse_char(cur, ']')) {
     52         cur->p = start;
     53         return 0;
     54     }
     55     
     56     block->type = BLOCK_MENTION_INDEX;
     57     block->block.mention_index = ind;
     58     
     59     return 1;
     60 }
     61 
     62 static int parse_hashtag(struct cursor *cur, struct note_block *block) {
     63     int c;
     64     u8 *start = cur->p;
     65     
     66     if (!parse_char(cur, '#'))
     67         return 0;
     68     
     69     c = peek_char(cur, 0);
     70     if (c == -1 || is_whitespace(c) || c == '#') {
     71         cur->p = start;
     72         return 0;
     73     }
     74     
     75     consume_until_boundary(cur);
     76     
     77     block->type = BLOCK_HASHTAG;
     78     block->block.str.start = (const char*)(start + 1);
     79     block->block.str.end = (const char*)cur->p;
     80     
     81     return 1;
     82 }
     83 
     84 static int add_block(struct note_blocks *blocks, struct note_block block)
     85 {
     86     if (blocks->num_blocks + 1 >= MAX_BLOCKS)
     87         return 0;
     88     
     89     blocks->blocks[blocks->num_blocks++] = block;
     90     return 1;
     91 }
     92 
     93 static int add_text_block(struct note_blocks *blocks, const u8 *start, const u8 *end)
     94 {
     95     struct note_block b;
     96     
     97     if (start == end)
     98         return 1;
     99     
    100     b.type = BLOCK_TEXT;
    101     b.block.str.start = (const char*)start;
    102     b.block.str.end = (const char*)end;
    103     
    104     return add_block(blocks, b);
    105 }
    106 
    107 static int consume_url_fragment(struct cursor *cur)
    108 {
    109     int c;
    110 
    111     if ((c = peek_char(cur, 0)) < 0)
    112         return 1;
    113 
    114     if (c != '#' && c != '?') {
    115         return 1;
    116     }
    117 
    118     cur->p++;
    119 
    120     return consume_until_end_url(cur, 1);
    121 }
    122 
    123 static int consume_url_path(struct cursor *cur)
    124 {
    125     int c;
    126 
    127     if ((c = peek_char(cur, 0)) < 0)
    128         return 1;
    129 
    130     if (c != '/') {
    131         return 1;
    132     }
    133 
    134     while (cur->p < cur->end) {
    135         c = *cur->p;
    136 
    137         if (c == '?' || c == '#' || is_final_url_char(cur->p, cur->end)) {
    138             return 1;
    139         }
    140 
    141         cur->p++;
    142     }
    143 
    144     return 1;
    145 }
    146 
    147 static int consume_url_host(struct cursor *cur)
    148 {
    149 	char c;
    150 	int count = 0;
    151 
    152 	while (cur->p < cur->end) {
    153 		c = *cur->p;
    154 		// TODO: handle IDNs
    155         if ((is_alphanumeric(c) || c == '.' || c == '-') && !is_final_url_char(cur->p, cur->end))
    156 		{
    157 			count++;
    158 			cur->p++;
    159 			continue;
    160 		}
    161 
    162 		return count != 0;
    163 	}
    164 
    165 
    166 	// this means the end of the URL hostname is the end of the buffer and we finished
    167 	return count != 0;
    168 }
    169 
    170 static int parse_url(struct cursor *cur, struct note_block *block) {
    171     u8 *start = cur->p;
    172     u8 *host;
    173     int host_len;
    174     struct cursor path_cur;
    175     
    176     if (!parse_str(cur, "http"))
    177         return 0;
    178     
    179     if (parse_char(cur, 's') || parse_char(cur, 'S')) {
    180         if (!parse_str(cur, "://")) {
    181             cur->p = start;
    182             return 0;
    183         }
    184     } else {
    185         if (!parse_str(cur, "://")) {
    186             cur->p = start;
    187             return 0;
    188         }
    189     }
    190 
    191     // make sure to save the hostname. We will use this to detect damus.io links
    192     host = cur->p;
    193 
    194     if (!consume_url_host(cur)) {
    195 	    cur->p = start;
    196 	    return 0;
    197     }
    198 
    199     // get the length of the host string
    200     host_len = (int)(cur->p - host);
    201 
    202     // save the current parse state so that we can continue from here when
    203     // parsing the bech32 in the damus.io link if we have it
    204     copy_cursor(cur, &path_cur);
    205 
    206     // skip leading /
    207     cursor_skip(&path_cur, 1);
    208 
    209     if (!consume_url_path(cur)) {
    210 	    cur->p = start;
    211 	    return 0;
    212     }
    213 
    214     if (!consume_url_fragment(cur)) {
    215 	    cur->p = start;
    216 	    return 0;
    217     }
    218 
    219     // smart parens
    220     if (start - 1 >= 0 &&
    221         start < cur->end &&
    222         *(start - 1) == '(' &&
    223         (cur->p - 1) < cur->end &&
    224         *(cur->p - 1) == ')')
    225     {
    226         cur->p--;
    227     }
    228 
    229     // save the bech32 string pos in case we hit a damus.io link
    230     block->block.str.start = (const char *)path_cur.p;
    231 
    232     // if we have a damus link, make it a mention
    233     if (host_len == 8
    234 	&& !strncmp((const char *)host, "damus.io", 8)
    235 	&& parse_nostr_bech32(&path_cur, &block->block.mention_bech32.bech32))
    236     {
    237 	    block->block.str.end = (const char *)path_cur.p;
    238 	    block->type = BLOCK_MENTION_BECH32;
    239 	    return 1;
    240     }
    241 
    242     block->type = BLOCK_URL;
    243     block->block.str.start = (const char *)start;
    244     block->block.str.end = (const char *)cur->p;
    245     
    246     return 1;
    247 }
    248 
    249 static int parse_invoice(struct cursor *cur, struct note_block *block) {
    250     u8 *start, *end;
    251     char *fail;
    252     struct bolt11 *bolt11;
    253     // optional
    254     parse_str(cur, "lightning:");
    255     
    256     start = cur->p;
    257     
    258     if (!parse_str(cur, "lnbc"))
    259         return 0;
    260     
    261     if (!consume_until_whitespace(cur, 1)) {
    262         cur->p = start;
    263         return 0;
    264     }
    265     
    266     end = cur->p;
    267     
    268     char str[end - start + 1];
    269     str[end - start] = 0;
    270     memcpy(str, start, end - start);
    271     
    272     if (!(bolt11 = bolt11_decode(NULL, str, &fail))) {
    273         cur->p = start;
    274         return 0;
    275     }
    276     
    277     block->type = BLOCK_INVOICE;
    278     
    279     block->block.invoice.invstr.start = (const char*)start;
    280     block->block.invoice.invstr.end = (const char*)end;
    281     block->block.invoice.bolt11 = bolt11;
    282     
    283     cur->p = end;
    284     
    285     return 1;
    286 }
    287 
    288 
    289 static int parse_mention_bech32(struct cursor *cur, struct note_block *block) {
    290     u8 *start = cur->p;
    291     
    292     parse_char(cur, '@');
    293     parse_str(cur, "nostr:");
    294 
    295     block->block.str.start = (const char *)cur->p;
    296     
    297     if (!parse_nostr_bech32(cur, &block->block.mention_bech32.bech32)) {
    298         cur->p = start;
    299         return 0;
    300     }
    301     
    302     block->block.str.end = (const char *)cur->p;
    303     
    304     block->type = BLOCK_MENTION_BECH32;
    305 
    306     return 1;
    307 }
    308 
    309 static int add_text_then_block(struct cursor *cur, struct note_blocks *blocks, struct note_block block, u8 **start, const u8 *pre_mention)
    310 {
    311     if (!add_text_block(blocks, *start, pre_mention))
    312         return 0;
    313     
    314     *start = (u8*)cur->p;
    315     
    316     if (!add_block(blocks, block))
    317         return 0;
    318     
    319     return 1;
    320 }
    321 
    322 int damus_parse_content(struct note_blocks *blocks, const char *content) {
    323     int cp, c;
    324     struct cursor cur;
    325     struct note_block block;
    326     u8 *start, *pre_mention;
    327     
    328     blocks->words = 0;
    329     blocks->num_blocks = 0;
    330     make_cursor((u8*)content, (u8*)content + strlen(content), &cur);
    331     
    332     start = cur.p;
    333     while (cur.p < cur.end && blocks->num_blocks < MAX_BLOCKS) {
    334         cp = peek_char(&cur, -1);
    335         c  = peek_char(&cur, 0);
    336         
    337         // new word
    338         if (is_whitespace(cp) && !is_whitespace(c)) {
    339             blocks->words++;
    340         }
    341         
    342         pre_mention = cur.p;
    343         if (cp == -1 || is_left_boundary(cp) || c == '#') {
    344             if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) {
    345                 if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
    346                     return 0;
    347                 continue;
    348             } else if ((c == 'h' || c == 'H') && parse_url(&cur, &block)) {
    349                 if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
    350                     return 0;
    351                 continue;
    352             } else if ((c == 'l' || c == 'L') && parse_invoice(&cur, &block)) {
    353                 if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
    354                     return 0;
    355                 continue;
    356             } else if ((c == 'n' || c == '@') && parse_mention_bech32(&cur, &block)) {
    357                 if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
    358                     return 0;
    359                 continue;
    360             }
    361         }
    362         
    363         cur.p++;
    364     }
    365     
    366     if (cur.p - start > 0) {
    367         if (!add_text_block(blocks, start, cur.p))
    368             return 0;
    369     }
    370     
    371     return 1;
    372 }
    373 
    374 void blocks_init(struct note_blocks *blocks) {
    375     blocks->blocks = malloc(sizeof(struct note_block) * MAX_BLOCKS);
    376     blocks->num_blocks = 0;
    377 }
    378 
    379 void blocks_free(struct note_blocks *blocks) {
    380     if (!blocks->blocks) {
    381         return;
    382     }
    383     
    384     for (int i = 0; i < blocks->num_blocks; ++i) {
    385         if (blocks->blocks[i].type == BLOCK_MENTION_BECH32) {
    386             free(blocks->blocks[i].block.mention_bech32.bech32.buffer);
    387             blocks->blocks[i].block.mention_bech32.bech32.buffer = NULL;
    388         }
    389     }
    390 
    391     free(blocks->blocks);
    392     blocks->num_blocks = 0;
    393 }