// nip19.ts import { bytesToHex as bytesToHex2, concatBytes, hexToBytes as hexToBytes2 } from "@noble/hashes/utils"; import { bech32 } from "@scure/base"; // utils.ts import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; var utf8Decoder = new TextDecoder("utf-8"); var utf8Encoder = new TextEncoder(); // nip19.ts var Bech32MaxSize = 5e3; function decode(code) { let { prefix, words } = bech32.decode(code, Bech32MaxSize); let data = new Uint8Array(bech32.fromWords(words)); switch (prefix) { case "nprofile": { let tlv = parseTLV(data); if (!tlv[0]?.[0]) throw new Error("missing TLV 0 for nprofile"); if (tlv[0][0].length !== 32) throw new Error("TLV 0 should be 32 bytes"); return { type: "nprofile", data: { pubkey: bytesToHex2(tlv[0][0]), relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [] } }; } case "nevent": { let tlv = parseTLV(data); if (!tlv[0]?.[0]) throw new Error("missing TLV 0 for nevent"); if (tlv[0][0].length !== 32) throw new Error("TLV 0 should be 32 bytes"); if (tlv[2] && tlv[2][0].length !== 32) throw new Error("TLV 2 should be 32 bytes"); if (tlv[3] && tlv[3][0].length !== 4) throw new Error("TLV 3 should be 4 bytes"); return { type: "nevent", data: { id: bytesToHex2(tlv[0][0]), relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [], author: tlv[2]?.[0] ? bytesToHex2(tlv[2][0]) : void 0, kind: tlv[3]?.[0] ? parseInt(bytesToHex2(tlv[3][0]), 16) : void 0 } }; } case "naddr": { let tlv = parseTLV(data); if (!tlv[0]?.[0]) throw new Error("missing TLV 0 for naddr"); if (!tlv[2]?.[0]) throw new Error("missing TLV 2 for naddr"); if (tlv[2][0].length !== 32) throw new Error("TLV 2 should be 32 bytes"); if (!tlv[3]?.[0]) throw new Error("missing TLV 3 for naddr"); if (tlv[3][0].length !== 4) throw new Error("TLV 3 should be 4 bytes"); return { type: "naddr", data: { identifier: utf8Decoder.decode(tlv[0][0]), pubkey: bytesToHex2(tlv[2][0]), kind: parseInt(bytesToHex2(tlv[3][0]), 16), relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [] } }; } case "nsec": return { type: prefix, data }; case "npub": case "note": return { type: prefix, data: bytesToHex2(data) }; default: throw new Error(`unknown prefix ${prefix}`); } } function parseTLV(data) { let result = {}; let rest = data; while (rest.length > 0) { let t = rest[0]; let l = rest[1]; let v = rest.slice(2, 2 + l); rest = rest.slice(2 + l); if (v.length < l) throw new Error(`not enough data to read on TLV ${t}`); result[t] = result[t] || []; result[t].push(v); } return result; } // nip27.ts var noCharacter = /\W/m; var noURLCharacter = /[^\w\/] |[^\w\/]$|$|,| /m; var MAX_HASHTAG_LENGTH = 42; function* parse(content) { let emojis = []; if (typeof content !== "string") { for (let i = 0; i < content.tags.length; i++) { const tag = content.tags[i]; if (tag[0] === "emoji" && tag.length >= 3) { emojis.push({ type: "emoji", shortcode: tag[1], url: tag[2] }); } } content = content.content; } const max = content.length; let prevIndex = 0; let index = 0; mainloop: while (index < max) { const u = content.indexOf(":", index); const h = content.indexOf("#", index); if (u === -1 && h === -1) { break mainloop; } if (u === -1 || h >= 0 && h < u) { if (h === 0 || content[h - 1] === " ") { const m = content.slice(h + 1, h + MAX_HASHTAG_LENGTH).match(noCharacter); const end = m ? h + 1 + m.index : max; yield { type: "text", text: content.slice(prevIndex, h) }; yield { type: "hashtag", value: content.slice(h + 1, end) }; index = end; prevIndex = index; continue mainloop; } index = h + 1; continue mainloop; } if (content.slice(u - 5, u) === "nostr") { const m = content.slice(u + 60).match(noCharacter); const end = m ? u + 60 + m.index : max; try { let pointer; let { data, type } = decode(content.slice(u + 1, end)); switch (type) { case "npub": pointer = { pubkey: data }; break; case "note": pointer = { id: data }; break; case "nsec": index = end + 1; continue; default: pointer = data; } if (prevIndex !== u - 5) { yield { type: "text", text: content.slice(prevIndex, u - 5) }; } yield { type: "reference", pointer }; index = end; prevIndex = index; continue mainloop; } catch (_err) { index = u + 1; continue mainloop; } } else if (content.slice(u - 5, u) === "https" || content.slice(u - 4, u) === "http") { const m = content.slice(u + 4).match(noURLCharacter); const end = m ? u + 4 + m.index : max; const prefixLen = content[u - 1] === "s" ? 5 : 4; try { let url = new URL(content.slice(u - prefixLen, end)); if (url.hostname.indexOf(".") === -1) { throw new Error("invalid url"); } if (prevIndex !== u - prefixLen) { yield { type: "text", text: content.slice(prevIndex, u - prefixLen) }; } if (/\.(png|jpe?g|gif|webp|heic|svg)$/i.test(url.pathname)) { yield { type: "image", url: url.toString() }; index = end; prevIndex = index; continue mainloop; } if (/\.(mp4|avi|webm|mkv|mov)$/i.test(url.pathname)) { yield { type: "video", url: url.toString() }; index = end; prevIndex = index; continue mainloop; } if (/\.(mp3|aac|ogg|opus|wav|flac)$/i.test(url.pathname)) { yield { type: "audio", url: url.toString() }; index = end; prevIndex = index; continue mainloop; } yield { type: "url", url: url.toString() }; index = end; prevIndex = index; continue mainloop; } catch (_err) { index = end + 1; continue mainloop; } } else if (content.slice(u - 3, u) === "wss" || content.slice(u - 2, u) === "ws") { const m = content.slice(u + 4).match(noURLCharacter); const end = m ? u + 4 + m.index : max; const prefixLen = content[u - 1] === "s" ? 3 : 2; try { let url = new URL(content.slice(u - prefixLen, end)); if (url.hostname.indexOf(".") === -1) { throw new Error("invalid ws url"); } if (prevIndex !== u - prefixLen) { yield { type: "text", text: content.slice(prevIndex, u - prefixLen) }; } yield { type: "relay", url: url.toString() }; index = end; prevIndex = index; continue mainloop; } catch (_err) { index = end + 1; continue mainloop; } } else { for (let e = 0; e < emojis.length; e++) { const emoji = emojis[e]; if (content[u + emoji.shortcode.length + 1] === ":" && content.slice(u + 1, u + emoji.shortcode.length + 1) === emoji.shortcode) { if (prevIndex !== u) { yield { type: "text", text: content.slice(prevIndex, u) }; } yield emoji; index = u + emoji.shortcode.length + 2; prevIndex = index; continue mainloop; } } index = u + 1; continue mainloop; } } if (prevIndex !== max) { yield { type: "text", text: content.slice(prevIndex) }; } } export { parse };