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