damus.io

damus.io website
git clone git://jb55.com/damus.io
Log | Files | Refs | README | LICENSE

nostr.js (7448B)


      1 const nostrjs = (function nostrlib() {
      2 const WS = typeof WebSocket !== 'undefined' ? WebSocket : require('ws')
      3 
      4 function RelayPool(relays, opts)
      5 {
      6 	if (!(this instanceof RelayPool))
      7 		return new RelayPool(relays)
      8 
      9 	this.onfn = {}
     10 	this.relays = []
     11 
     12 	for (const relay of relays) {
     13 		this.add(relay)
     14 	}
     15 
     16 	return this
     17 }
     18 
     19 RelayPool.prototype.close = function relayPoolClose() {
     20 	for (const relay of this.relays) {
     21 		relay.close()
     22 	}
     23 }
     24 
     25 RelayPool.prototype.on = function relayPoolOn(method, fn) {
     26 	for (const relay of this.relays) {
     27 		this.onfn[method] = fn
     28 		relay.onfn[method] = fn.bind(null, relay)
     29 	}
     30 }
     31 
     32 RelayPool.prototype.has = function relayPoolHas(relayUrl) {
     33 	for (const relay of this.relays) {
     34 		if (relay.url === relayUrl)
     35 			return true
     36 	}
     37 
     38 	return false
     39 }
     40 
     41 RelayPool.prototype.setupHandlers = function relayPoolSetupHandlers()
     42 {
     43 	// setup its message handlers with the ones we have already
     44 	const keys = Object.keys(this.onfn)
     45 	for (const handler of keys) {
     46 		for (const relay of this.relays) {
     47 			relay.onfn[handler] = this.onfn[handler].bind(null, relay)
     48 		}
     49 	}
     50 }
     51 
     52 RelayPool.prototype.remove = function relayPoolRemove(url) {
     53 	let i = 0
     54 
     55 	for (const relay of this.relays) {
     56 		if (relay.url === url) {
     57 			relay.ws && relay.ws.close()
     58 			this.relays = this.replays.splice(i, 1)
     59 			return true
     60 		}
     61 
     62 		i += 1
     63 	}
     64 
     65 	return false
     66 }
     67 
     68 RelayPool.prototype.subscribe = function relayPoolSubscribe(sub_id, filters, relay_ids) {
     69 	const relays = relay_ids ? this.find_relays(relay_ids) : this.relays
     70 	for (const relay of relays) {
     71 		relay.subscribe(sub_id, filters)
     72 	}
     73 }
     74 
     75 RelayPool.prototype.unsubscribe = function relayPoolUnsubscibe(sub_id, relay_ids) {
     76 	const relays = relay_ids ? this.find_relays(relay_ids) : this.relays
     77 	for (const relay of relays) {
     78 		relay.unsubscribe(sub_id)
     79 	}
     80 }
     81 
     82 RelayPool.prototype.send = function relayPoolSend(payload, relay_ids) {
     83 	const relays = relay_ids ? this.find_relays(relay_ids) : this.relays
     84 	for (const relay of relays) {
     85 		relay.send(payload)
     86 	}
     87 }
     88 
     89 RelayPool.prototype.add = function relayPoolAdd(relay) {
     90 	if (relay instanceof Relay) {
     91 		if (this.has(relay.url))
     92 			return false
     93 
     94 		this.relays.push(relay)
     95 		this.setupHandlers()
     96 		return true
     97 	}
     98 
     99 	if (this.has(relay))
    100 		return false
    101 
    102 	const r = Relay(relay, this.opts)
    103 	this.relays.push(r)
    104 	this.setupHandlers()
    105 	return true
    106 }
    107 
    108 RelayPool.prototype.find_relays = function relayPoolFindRelays(relay_ids) {
    109 	if (relay_ids instanceof Relay)
    110 		return [relay_ids]
    111 
    112 	if (relay_ids.length === 0)
    113 		return []
    114 
    115 	if (!relay_ids[0])
    116 		throw new Error("what!?")
    117 
    118 	if (relay_ids[0] instanceof Relay)
    119 		return relay_ids
    120 
    121 	return this.relays.reduce((acc, relay) => {
    122 		if (relay_ids.some((rid) => relay.url === rid))
    123 			acc.push(relay)
    124 		return acc
    125 	}, [])
    126 }
    127 
    128 Relay.prototype.wait_connected = async function relay_wait_connected(data) {
    129 	let retry = 1000
    130 	while (true) {
    131 		if (!this.ws || this.ws.readyState !== 1) {
    132 			await sleep(retry)
    133 			retry *= 1.5
    134 		}
    135 		else {
    136 			return
    137 		}
    138 	}
    139 }
    140 
    141 
    142 function Relay(relay, opts={})
    143 {
    144 	if (!(this instanceof Relay))
    145 		return new Relay(relay, opts)
    146 
    147 	this.url = relay
    148 	this.opts = opts
    149 
    150 	if (opts.reconnect == null)
    151 		opts.reconnect = true
    152 
    153 	const me = this
    154 	me.onfn = {}
    155 
    156 	try {
    157 		init_websocket(me)
    158 	} catch (e) {
    159 		console.log(e)
    160 	}
    161 
    162 	return this
    163 }
    164 
    165 function init_websocket(me) {
    166 	let ws
    167 	try {
    168 		ws = me.ws = new WS(me.url);
    169 	} catch(e) {
    170 		return null
    171 	}
    172 	return new Promise((resolve, reject) => {
    173 		let resolved = false
    174 		ws.onmessage = (m) => { handle_nostr_message(me, m) }
    175 		ws.onclose = () => { 
    176 			if (me.onfn.close) 
    177 				me.onfn.close() 
    178 			if (me.reconnecting)
    179 				return reject(new Error("close during reconnect"))
    180 			if (!me.manualClose && me.opts.reconnect)
    181 				reconnect(me)
    182 		}
    183 		ws.onerror = () => { 
    184 			if (me.onfn.error)
    185 				me.onfn.error() 
    186 			if (me.reconnecting)
    187 				return reject(new Error("error during reconnect"))
    188 			if (me.opts.reconnect)
    189 				reconnect(me)
    190 		}
    191 		ws.onopen = () => {
    192 			if (me.onfn.open)
    193 				me.onfn.open()
    194 			else
    195 				console.log("no onopen???", me)
    196 
    197 			if (resolved) return
    198 
    199 			resolved = true
    200 			resolve(me)
    201 		}
    202 	});
    203 }
    204 
    205 function sleep(ms) {
    206     return new Promise(resolve => setTimeout(resolve, ms));
    207 }
    208 
    209 async function reconnect(me)
    210 {
    211 	const reconnecting = true
    212 	let n = 100
    213 	try {
    214 		me.reconnecting = true
    215 		await init_websocket(me)
    216 		me.reconnecting = false
    217 	} catch {
    218 		//console.error(`error thrown during reconnect... trying again in ${n} ms`)
    219 		await sleep(n)
    220 		n *= 1.5
    221 	}
    222 }
    223 
    224 Relay.prototype.on = function relayOn(method, fn) {
    225 	this.onfn[method] = fn
    226 }
    227 
    228 Relay.prototype.close = function relayClose() {
    229 	if (this.ws) {
    230 		this.manualClose = true
    231 		this.ws.close()
    232 	}
    233 }
    234 
    235 Relay.prototype.subscribe = function relay_subscribe(sub_id, filters) {
    236 	if (Array.isArray(filters))
    237 		this.send(["REQ", sub_id, ...filters])
    238 	else
    239 		this.send(["REQ", sub_id, filters])
    240 }
    241 
    242 Relay.prototype.unsubscribe = function relay_unsubscribe(sub_id) {
    243 	this.send(["CLOSE", sub_id])
    244 }
    245 
    246 Relay.prototype.send = async function relay_send(data) {
    247 	await this.wait_connected()
    248 	this.ws.send(JSON.stringify(data))
    249 }
    250 
    251 function handle_nostr_message(relay, msg)
    252 {
    253 	let data
    254 	try {
    255 		data = JSON.parse(msg.data)
    256 	} catch (e) {
    257 		console.error("handle_nostr_message", msg, e)
    258 		return
    259 	}
    260 	if (data.length >= 2) {
    261 		switch (data[0]) {
    262 		case "EVENT":
    263 			if (data.length < 3)
    264 				return
    265 			return relay.onfn.event && relay.onfn.event(data[1], data[2])
    266 		case "EOSE":
    267 			return relay.onfn.eose && relay.onfn.eose(data[1])
    268 		case "NOTICE":
    269 			return relay.onfn.notice && relay.onfn.notice(...data.slice(1))
    270 		}
    271 	}
    272 }
    273 
    274 async function sha256(message) {
    275 	if (crypto.subtle) {
    276 		const buffer = await crypto.subtle.digest('SHA-256', message);
    277 		return new Uint8Array(buffer);
    278 	} else if (require) {
    279 		const { createHash } = require('crypto');
    280 		const hash = createHash('sha256');
    281 		[message].forEach((m) => hash.update(m));
    282 		return Uint8Array.from(hash.digest());
    283 	} else {
    284 		throw new Error("The environment doesn't have sha256 function");
    285 	}
    286 }
    287 
    288 async function calculate_id(ev) {
    289 	const commit = event_commitment(ev)
    290 	const buf = new TextEncoder().encode(commit);                    
    291 	return hex_encode(await sha256(buf))
    292 }
    293 
    294 function event_commitment(ev) {
    295 	const {pubkey,created_at,kind,tags,content} = ev
    296 	return JSON.stringify([0, pubkey, created_at, kind, tags, content])
    297 }
    298 
    299 function hex_char(val) {
    300 	if (val < 10)
    301 		return String.fromCharCode(48 + val)
    302 	if (val < 16)
    303 		return String.fromCharCode(97 + val - 10)
    304 }
    305 
    306 function hex_encode(buf) {
    307 	let str = ""
    308 	for (let i = 0; i < buf.length; i++) {
    309 		const c = buf[i]
    310 		str += hex_char(c >> 4)
    311 		str += hex_char(c & 0xF)
    312 	}
    313 	return str
    314 }
    315 
    316 function char_to_hex(cstr) {
    317 	const c = cstr.charCodeAt(0)
    318 	// c >= 0 && c <= 9
    319 	if (c >= 48 && c <= 57) {
    320 		return c - 48;
    321 	}
    322 	// c >= a && c <= f
    323  	if (c >= 97 && c <= 102) {
    324 		return c - 97 + 10;
    325 	}
    326 	// c >= A && c <= F
    327  	if (c >= 65 && c <= 70) {
    328 		return c - 65 + 10;
    329 	}
    330 	return -1;
    331 }
    332 
    333 
    334 function hex_decode(str, buflen)
    335 {
    336 	let bufsize = buflen || 33
    337 	let c1, c2
    338 	let i = 0
    339 	let j = 0
    340 	let buf = new Uint8Array(bufsize)
    341 	let slen = str.length
    342 	while (slen > 1) {
    343 		if (-1==(c1 = char_to_hex(str[j])) || -1==(c2 = char_to_hex(str[j+1])))
    344 			return null;
    345 		if (!bufsize)
    346 			return null;
    347 		j += 2
    348 		slen -= 2
    349 		buf[i++] = (c1 << 4) | c2
    350 		bufsize--;
    351 	}
    352 
    353 	return buf
    354 }
    355 
    356 return {
    357 	RelayPool,
    358 	calculate_id,
    359 	event_commitment,
    360 	hex_encode,
    361 	hex_decode,
    362 }
    363 })()
    364 
    365 if (typeof module !== 'undefined' && module.exports)
    366 	module.exports = nostrjs