You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
246 lines
8.0 KiB
246 lines
8.0 KiB
// 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 |
|
};
|
|
|