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 }