yosup/js/nostr.js
Thomas Mathews e3b31af127 Added basic settings support.
You can edit relays, embed options, and sign out. This moves the signout
button from the nav to the settings area.
2022-12-29 21:56:06 -08:00

369 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.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))
case "OK":
return relay.onfn.ok && relay.onfn.ok(...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