yosup/web/js/nostr.js
William Casarin 45b36a9945 Unknown event fetching
Damus will now try really hard to find unknown events. It will use the
event relay hints to connect to new relays and fetch any id that is
missing.
2022-11-20 13:35:44 -08:00

368 lines
7.4 KiB
JavaScript

const nostrjs = (function nostrlib() {
const WS = typeof WebSocket !== 'undefined' ? WebSocket : require('ws')
function RelayPool(relays, opts)
{
if (!(this instanceof RelayPool))
return new RelayPool(relays)
this.onfn = {}
this.relays = []
for (const relay of relays) {
this.add(relay)
}
return this
}
RelayPool.prototype.close = function relayPoolClose() {
for (const relay of this.relays) {
relay.close()
}
}
RelayPool.prototype.on = function relayPoolOn(method, fn) {
for (const relay of this.relays) {
this.onfn[method] = fn
relay.onfn[method] = fn.bind(null, relay)
}
}
RelayPool.prototype.has = function relayPoolHas(relayUrl) {
for (const relay of this.relays) {
if (relay.url === relayUrl)
return true
}
return false
}
RelayPool.prototype.setupHandlers = function relayPoolSetupHandlers()
{
// setup its message handlers with the ones we have already
const keys = Object.keys(this.onfn)
for (const handler of keys) {
for (const relay of this.relays) {
relay.onfn[handler] = this.onfn[handler].bind(null, relay)
}
}
}
RelayPool.prototype.remove = function relayPoolRemove(url) {
let i = 0
for (const relay of this.relays) {
if (relay.url === url) {
relay.ws && relay.ws.close()
this.relays = this.replays.splice(i, 1)
return true
}
i += 1
}
return false
}
RelayPool.prototype.subscribe = function relayPoolSubscribe(sub_id, filters, relay_ids) {
const relays = relay_ids ? this.find_relays(relay_ids) : this.relays
for (const relay of relays) {
relay.subscribe(sub_id, filters)
}
}
RelayPool.prototype.unsubscribe = function relayPoolUnsubscibe(sub_id, relay_ids) {
const relays = relay_ids ? this.find_relays(relay_ids) : this.relays
for (const relay of relays) {
relay.unsubscribe(sub_id)
}
}
RelayPool.prototype.send = function relayPoolSend(payload, relay_ids) {
const relays = relay_ids ? this.find_relays(relay_ids) : this.relays
for (const relay of relays) {
relay.send(payload)
}
}
RelayPool.prototype.add = function relayPoolAdd(relay) {
if (relay instanceof Relay) {
if (this.has(relay.url))
return false
this.relays.push(relay)
this.setupHandlers()
return true
}
if (this.has(relay))
return false
const r = Relay(relay, this.opts)
this.relays.push(r)
this.setupHandlers()
return true
}
RelayPool.prototype.find_relays = function relayPoolFindRelays(relay_ids) {
if (relay_ids instanceof Relay)
return [relay_ids]
if (relay_ids.length === 0)
return []
if (!relay_ids[0])
throw new Error("what!?")
if (relay_ids[0] instanceof Relay)
return relay_ids
return this.relays.reduce((acc, relay) => {
if (relay_ids.some((rid) => relay.url === rid))
acc.push(relay)
return acc
}, [])
}
Relay.prototype.wait_connected = async function relay_wait_connected(data) {
let retry = 1000
while (true) {
if (!this.ws || this.ws.readyState !== 1) {
await sleep(retry)
retry *= 1.5
}
else {
return
}
}
}
function Relay(relay, opts={})
{
if (!(this instanceof Relay))
return new Relay(relay, opts)
this.url = relay
this.opts = opts
if (opts.reconnect == null)
opts.reconnect = true
const me = this
me.onfn = {}
try {
init_websocket(me)
} catch (e) {
console.log(e)
}
return this
}
function init_websocket(me) {
let ws
try {
ws = me.ws = new WS(me.url);
} catch(e) {
return null
}
return new Promise((resolve, reject) => {
let resolved = false
ws.onmessage = (m) => { handle_nostr_message(me, m) }
ws.onclose = () => {
if (me.onfn.close)
me.onfn.close()
if (me.reconnecting)
return reject(new Error("close during reconnect"))
if (!me.manualClose && me.opts.reconnect)
reconnect(me)
}
ws.onerror = () => {
if (me.onfn.error)
me.onfn.error()
if (me.reconnecting)
return reject(new Error("error during reconnect"))
if (me.opts.reconnect)
reconnect(me)
}
ws.onopen = () => {
if (me.onfn.open)
me.onfn.open()
else
console.log("no onopen???", me)
if (resolved) return
resolved = true
resolve(me)
}
});
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function reconnect(me)
{
const reconnecting = true
let n = 100
try {
me.reconnecting = true
await init_websocket(me)
me.reconnecting = false
} catch {
//console.error(`error thrown during reconnect... trying again in ${n} ms`)
await sleep(n)
n *= 1.5
}
}
Relay.prototype.on = function relayOn(method, fn) {
this.onfn[method] = fn
}
Relay.prototype.close = function relayClose() {
if (this.ws) {
this.manualClose = true
this.ws.close()
}
}
Relay.prototype.subscribe = function relay_subscribe(sub_id, filters) {
//console.debug("[%s] %s %s", this.url, sub_id, filters)
if (Array.isArray(filters))
this.send(["REQ", sub_id, ...filters])
else
this.send(["REQ", sub_id, filters])
}
Relay.prototype.unsubscribe = function relay_unsubscribe(sub_id) {
//console.debug("[%s] CLOSE %s", this.url, sub_id)
this.send(["CLOSE", sub_id])
}
Relay.prototype.send = async function relay_send(data) {
await this.wait_connected()
this.ws.send(JSON.stringify(data))
}
function handle_nostr_message(relay, msg)
{
let data
try {
data = JSON.parse(msg.data)
} catch (e) {
console.error("handle_nostr_message", msg, e)
return
}
if (data.length >= 2) {
switch (data[0]) {
case "EVENT":
if (data.length < 3)
return
return relay.onfn.event && relay.onfn.event(data[1], data[2])
case "EOSE":
return relay.onfn.eose && relay.onfn.eose(data[1])
case "NOTICE":
return relay.onfn.notice && relay.onfn.notice(...data.slice(1))
}
}
}
async function sha256(message) {
if (crypto.subtle) {
const buffer = await crypto.subtle.digest('SHA-256', message);
return new Uint8Array(buffer);
} else if (require) {
const { createHash } = require('crypto');
const hash = createHash('sha256');
[message].forEach((m) => hash.update(m));
return Uint8Array.from(hash.digest());
} else {
throw new Error("The environment doesn't have sha256 function");
}
}
async function calculate_id(ev) {
const commit = event_commitment(ev)
const buf = new TextEncoder().encode(commit);
return hex_encode(await sha256(buf))
}
function event_commitment(ev) {
const {pubkey,created_at,kind,tags,content} = ev
return JSON.stringify([0, pubkey, created_at, kind, tags, content])
}
function hex_char(val) {
if (val < 10)
return String.fromCharCode(48 + val)
if (val < 16)
return String.fromCharCode(97 + val - 10)
}
function hex_encode(buf) {
let str = ""
for (let i = 0; i < buf.length; i++) {
const c = buf[i]
str += hex_char(c >> 4)
str += hex_char(c & 0xF)
}
return str
}
function char_to_hex(cstr) {
const c = cstr.charCodeAt(0)
// c >= 0 && c <= 9
if (c >= 48 && c <= 57) {
return c - 48;
}
// c >= a && c <= f
if (c >= 97 && c <= 102) {
return c - 97 + 10;
}
// c >= A && c <= F
if (c >= 65 && c <= 70) {
return c - 65 + 10;
}
return -1;
}
function hex_decode(str, buflen)
{
let bufsize = buflen || 33
let c1, c2
let i = 0
let j = 0
let buf = new Uint8Array(bufsize)
let slen = str.length
while (slen > 1) {
if (-1==(c1 = char_to_hex(str[j])) || -1==(c2 = char_to_hex(str[j+1])))
return null;
if (!bufsize)
return null;
j += 2
slen -= 2
buf[i++] = (c1 << 4) | c2
bufsize--;
}
return buf
}
return {
RelayPool,
calculate_id,
event_commitment,
hex_encode,
hex_decode,
}
})()
if (typeof module !== 'undefined' && module.exports)
module.exports = nostrjs