forked from nostr/nostrweb
relays: type and upgrade to nostr-tools@1.6.0
move and typed relay related code to relays.ts upgrade nostr-tools to latest greatest, major version with breaking changes: - relayPool is gone in favor of SimplePool, but this commit just used relayInit directly as relays should become configurable at some point
parent
57be701ef9
commit
87cd5f21b3
File diff suppressed because it is too large
Load Diff
|
@ -8,7 +8,10 @@
|
||||||
"esbuild": "^0.14.54",
|
"esbuild": "^0.14.54",
|
||||||
"esbuild-plugin-alias": "^0.2.1",
|
"esbuild-plugin-alias": "^0.2.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"nostr-tools": "0.24.1"
|
"readable-stream": "4.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"nostr-tools": "1.6.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node tools/build.js",
|
"build": "node tools/build.js",
|
||||||
|
|
142
src/main.js
142
src/main.js
|
@ -1,22 +1,11 @@
|
||||||
import {relayPool, generatePrivateKey, getPublicKey, signEvent} from 'nostr-tools';
|
import {generatePrivateKey, getEventHash, getPublicKey, signEvent} from 'nostr-tools';
|
||||||
|
import {publish, sub, unsubAll} from './relays';
|
||||||
import {bounce} from './utils.js';
|
import {bounce} from './utils.js';
|
||||||
import {zeroLeadingBitsCount} from './cryptoutils.js';
|
import {zeroLeadingBitsCount} from './cryptoutils.js';
|
||||||
import {elem, parseTextContent} from './domutil.js';
|
import {elem, parseTextContent} from './domutil.js';
|
||||||
import {dateTime, formatTime} from './timeutil.js';
|
import {dateTime, formatTime} from './timeutil.js';
|
||||||
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
|
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
|
||||||
|
|
||||||
const pool = relayPool();
|
|
||||||
// pool.addRelay('wss://relay.nostr.info', {read: true, write: true});
|
|
||||||
// pool.addRelay('wss://relay.damus.io', {read: true, write: true});
|
|
||||||
// pool.addRelay('wss://relay.snort.social', {read: true, write: true});
|
|
||||||
|
|
||||||
pool.addRelay('wss://relay.nostr.ch', {read: true, write: true});
|
|
||||||
pool.addRelay('wss://nostr.openchain.fr', {read: true, write: true});
|
|
||||||
pool.addRelay('wss://eden.nostr.land', {read: true, write: true});
|
|
||||||
pool.addRelay('wss://nostr.einundzwanzig.space', {read: true, write: true});
|
|
||||||
pool.addRelay('wss://relay.nostrich.de', {read: true, write: true});
|
|
||||||
pool.addRelay('wss://nostr.cercatrova.me', {read: true, write: true});
|
|
||||||
|
|
||||||
function onEvent(evt, relay) {
|
function onEvent(evt, relay) {
|
||||||
switch (evt.kind) {
|
switch (evt.kind) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -46,14 +35,8 @@ let pubkey = localStorage.getItem('pub_key') || (() => {
|
||||||
return pubkey;
|
return pubkey;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const subList = [];
|
|
||||||
const unSubAll = () => {
|
|
||||||
subList.forEach(sub => sub.unsub());
|
|
||||||
subList.length = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
function sub24hFeed() {
|
function sub24hFeed() {
|
||||||
subList.push(pool.sub({
|
sub({
|
||||||
cb: onEvent,
|
cb: onEvent,
|
||||||
filter: {
|
filter: {
|
||||||
kinds: [0, 1, 2, 7],
|
kinds: [0, 1, 2, 7],
|
||||||
|
@ -61,7 +44,7 @@ function sub24hFeed() {
|
||||||
since: Math.floor((Date.now() * 0.001) - (24 * 60 * 60)),
|
since: Math.floor((Date.now() * 0.001) - (24 * 60 * 60)),
|
||||||
limit: 50,
|
limit: 50,
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function subNoteAndProfile(id) {
|
function subNoteAndProfile(id) {
|
||||||
|
@ -71,18 +54,18 @@ function subNoteAndProfile(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function subTextNote(eventId) {
|
function subTextNote(eventId) {
|
||||||
subList.push(pool.sub({
|
sub({
|
||||||
cb: onEvent,
|
cb: onEvent,
|
||||||
filter: {
|
filter: {
|
||||||
ids: [eventId],
|
ids: [eventId],
|
||||||
kinds: [1],
|
kinds: [1],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function subProfile(pubkey) {
|
function subProfile(pubkey) {
|
||||||
subList.push(pool.sub({
|
sub({
|
||||||
cb: (evt, relay) => {
|
cb: (evt, relay) => {
|
||||||
console.log('found profile, unsub subTextNote somehow')
|
console.log('found profile, unsub subTextNote somehow')
|
||||||
// renderProfile(evt, relay);
|
// renderProfile(evt, relay);
|
||||||
|
@ -93,16 +76,16 @@ function subProfile(pubkey) {
|
||||||
kinds: [0],
|
kinds: [0],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
// get notes for profile
|
// get notes for profile
|
||||||
subList.push(pool.sub({
|
sub({
|
||||||
cb: onEvent,
|
cb: onEvent,
|
||||||
filter: {
|
filter: {
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [1],
|
kinds: [1],
|
||||||
limit: 50,
|
limit: 50,
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const containers = [
|
const containers = [
|
||||||
|
@ -115,39 +98,6 @@ const containers = [
|
||||||
];
|
];
|
||||||
let activeContainerIndex = null;
|
let activeContainerIndex = null;
|
||||||
|
|
||||||
// const profileContainer = document.querySelector('#profile');
|
|
||||||
// const profileAbout = profileContainer.querySelector('.profile-about');
|
|
||||||
// const profileName = profileContainer.querySelector('.profile-name');
|
|
||||||
// const profilePubkey = profileContainer.querySelector('.profile-pubkey');
|
|
||||||
// const profilePubkeyLabel = profileContainer.querySelector('.profile-pubkey-label');
|
|
||||||
// const profileImage = profileContainer.querySelector('.profile-image');
|
|
||||||
// const textNoteContainer = document.querySelector('#textnote');
|
|
||||||
|
|
||||||
// function clearProfile() {
|
|
||||||
// profileAbout.textContent = '';
|
|
||||||
// profileName.textContent = '';
|
|
||||||
// profilePubkey.textContent = '';
|
|
||||||
// profilePubkeyLabel.hidden = true;
|
|
||||||
// profileImage.removeAttribute('src');
|
|
||||||
// profileImage.hidden = true;
|
|
||||||
// }
|
|
||||||
// function renderProfile(evt, relay) {
|
|
||||||
// profileContainer.dataset.pubkey = evt.pubkey;
|
|
||||||
// profilePubkey.textContent = evt.pubkey;
|
|
||||||
// profilePubkeyLabel.hidden = false;
|
|
||||||
// const content = parseContent(evt.content);
|
|
||||||
// if (content) {
|
|
||||||
// profileAbout.textContent = content.about;
|
|
||||||
// profileName.textContent = content.name;
|
|
||||||
// const noxyImg = validatePow(evt) && getNoxyUrl('data', content.picture, evt.id, relay);
|
|
||||||
// if (noxyImg) {
|
|
||||||
// profileImage.setAttribute('src', noxyImg);
|
|
||||||
// profileImage.hidden = false;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
const textNoteList = []; // could use indexDB
|
const textNoteList = []; // could use indexDB
|
||||||
const eventRelayMap = {}; // eventId: [relay1, relay2]
|
const eventRelayMap = {}; // eventId: [relay1, relay2]
|
||||||
|
|
||||||
|
@ -356,6 +306,8 @@ function linkPreview(href, id, relay) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const writeInput = document.querySelector('textarea[name="message"]');
|
||||||
|
|
||||||
function createTextNote(evt, relay) {
|
function createTextNote(evt, relay) {
|
||||||
const {host, img, name, time, userName} = getMetadata(evt, relay);
|
const {host, img, name, time, userName} = getMetadata(evt, relay);
|
||||||
const replies = replyList.filter(({replyTo}) => replyTo === evt.id);
|
const replies = replyList.filter(({replyTo}) => replyTo === evt.id);
|
||||||
|
@ -625,7 +577,6 @@ function getReplyTo(evt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeForm = document.querySelector('#writeForm');
|
const writeForm = document.querySelector('#writeForm');
|
||||||
const writeInput = document.querySelector('textarea[name="message"]');
|
|
||||||
|
|
||||||
const elemShrink = () => {
|
const elemShrink = () => {
|
||||||
const height = writeInput.style.height || writeInput.getBoundingClientRect().height;
|
const height = writeInput.style.height || writeInput.getBoundingClientRect().height;
|
||||||
|
@ -718,17 +669,16 @@ async function upvote(eventId, eventPubkey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const privatekey = localStorage.getItem('private_key');
|
const privatekey = localStorage.getItem('private_key');
|
||||||
const sig = await signEvent(newReaction, privatekey).catch(console.error);
|
const sig = signEvent(newReaction, privatekey);
|
||||||
|
// TODO: validateEvent
|
||||||
if (sig) {
|
if (sig) {
|
||||||
statusElem.textContent = 'publishing…';
|
statusElem.textContent = 'publishing…';
|
||||||
const ev = await pool.publish({...newReaction, sig}, (status, url) => {
|
publish({...newReaction, sig}, (relay, error) => {
|
||||||
if (status === 0) {
|
if (error) {
|
||||||
console.info(`publish request sent to ${url}`);
|
return console.error(error, relay);
|
||||||
}
|
}
|
||||||
if (status === 1) {
|
console.info(`event published by ${relay}`);
|
||||||
console.info(`event published by ${url}`);
|
});
|
||||||
}
|
|
||||||
}).catch(console.error);
|
|
||||||
reactionBtn.disabled = false;
|
reactionBtn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -736,7 +686,7 @@ async function upvote(eventId, eventPubkey) {
|
||||||
// send
|
// send
|
||||||
const sendStatus = document.querySelector('#sendstatus');
|
const sendStatus = document.querySelector('#sendstatus');
|
||||||
const onSendError = err => sendStatus.textContent = err.message;
|
const onSendError = err => sendStatus.textContent = err.message;
|
||||||
const publish = document.querySelector('#publish');
|
const publishBtn = document.querySelector('#publish');
|
||||||
writeForm.addEventListener('submit', async (e) => {
|
writeForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// const pubkey = localStorage.getItem('pub_key');
|
// const pubkey = localStorage.getItem('pub_key');
|
||||||
|
@ -753,7 +703,7 @@ writeForm.addEventListener('submit', async (e) => {
|
||||||
sendStatus.textContent = '';
|
sendStatus.textContent = '';
|
||||||
writeInput.value = '';
|
writeInput.value = '';
|
||||||
writeInput.style.removeProperty('height');
|
writeInput.style.removeProperty('height');
|
||||||
publish.disabled = true;
|
publishBtn.disabled = true;
|
||||||
if (replyTo) {
|
if (replyTo) {
|
||||||
localStorage.removeItem('reply_to');
|
localStorage.removeItem('reply_to');
|
||||||
publishView.append(writeForm);
|
publishView.append(writeForm);
|
||||||
|
@ -772,23 +722,22 @@ writeForm.addEventListener('submit', async (e) => {
|
||||||
close();
|
close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sig = await signEvent(newEvent, privatekey).catch(onSendError);
|
const sig = signEvent(newEvent, privatekey);
|
||||||
|
// TODO validateEvent
|
||||||
if (sig) {
|
if (sig) {
|
||||||
sendStatus.textContent = 'publishing…';
|
sendStatus.textContent = 'publishing…';
|
||||||
const ev = await pool.publish({...newEvent, sig}, (status, url) => {
|
publish({...newEvent, sig}, (relay, error) => {
|
||||||
if (status === 0) {
|
if (error) {
|
||||||
console.info(`publish request sent to ${url}`);
|
return console.log(error, relay);
|
||||||
}
|
|
||||||
if (status === 1) {
|
|
||||||
close();
|
|
||||||
// console.info(`event published by ${url}`, ev);
|
|
||||||
}
|
}
|
||||||
|
console.info(`publish request sent to ${relay}`);
|
||||||
|
close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
writeInput.addEventListener('input', () => {
|
writeInput.addEventListener('input', () => {
|
||||||
publish.disabled = !writeInput.value.trimRight();
|
publishBtn.disabled = !writeInput.value.trimRight();
|
||||||
updateElemHeight(writeInput);
|
updateElemHeight(writeInput);
|
||||||
});
|
});
|
||||||
writeInput.addEventListener('blur', () => sendStatus.textContent = '');
|
writeInput.addEventListener('blur', () => sendStatus.textContent = '');
|
||||||
|
@ -918,7 +867,7 @@ switch(location.pathname) {
|
||||||
|
|
||||||
window.addEventListener('popstate', (event) => {
|
window.addEventListener('popstate', (event) => {
|
||||||
// console.log(`popstate: ${location.pathname}, state: ${JSON.stringify(event.state)}`);
|
// console.log(`popstate: ${location.pathname}, state: ${JSON.stringify(event.state)}`);
|
||||||
unSubAll();
|
unsubAll();
|
||||||
if (event.state?.pubkey) {
|
if (event.state?.pubkey) {
|
||||||
subProfile(event.state.pubkey);
|
subProfile(event.state.pubkey);
|
||||||
view(`/${event.state.pubkey}`);
|
view(`/${event.state.pubkey}`);
|
||||||
|
@ -961,18 +910,18 @@ document.body.addEventListener('click', (e) => {
|
||||||
switch(href) {
|
switch(href) {
|
||||||
case '/':
|
case '/':
|
||||||
navigate('/');
|
navigate('/');
|
||||||
unSubAll();
|
unsubAll();
|
||||||
sub24hFeed();
|
sub24hFeed();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
switch(a.dataset.nav) {
|
switch(a.dataset.nav) {
|
||||||
case '/[profile]':
|
case '/[profile]':
|
||||||
unSubAll();
|
unsubAll();
|
||||||
subProfile(pubkey);
|
subProfile(pubkey);
|
||||||
navigate({pubkey});
|
navigate({pubkey});
|
||||||
break;
|
break;
|
||||||
case '/[note]':
|
case '/[note]':
|
||||||
unSubAll();
|
unsubAll();
|
||||||
subTextNote(id)
|
subTextNote(id)
|
||||||
navigate({note: id});
|
navigate({note: id});
|
||||||
break;
|
break;
|
||||||
|
@ -1144,18 +1093,18 @@ profileForm.addEventListener('submit', async (e) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const privatekey = localStorage.getItem('private_key');
|
const privatekey = localStorage.getItem('private_key');
|
||||||
const sig = await signEvent(newProfile, privatekey).catch(console.error);
|
const sig = signEvent(newProfile, privatekey);
|
||||||
|
// TODO: validateEvent
|
||||||
if (sig) {
|
if (sig) {
|
||||||
const ev = await pool.publish({...newProfile, sig}, (status, url) => {
|
publish({...newProfile, sig}, (relay, error) => {
|
||||||
if (status === 0) {
|
if (error) {
|
||||||
console.info(`publish request sent to ${url}`);
|
return console.error(error, relay);
|
||||||
}
|
}
|
||||||
if (status === 1) {
|
console.info(`publish request sent to ${relay}`);
|
||||||
profileStatus.textContent = 'profile metadata successfully published';
|
profileStatus.textContent = 'profile metadata successfully published';
|
||||||
profileStatus.hidden = false;
|
profileStatus.hidden = false;
|
||||||
profileSubmit.disabled = true;
|
profileSubmit.disabled = true;
|
||||||
}
|
});
|
||||||
}).catch(console.error);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1223,7 +1172,10 @@ function validatePow(evt) {
|
||||||
function powEvent(evt, options) {
|
function powEvent(evt, options) {
|
||||||
const {difficulty, statusElem, timeout} = options;
|
const {difficulty, statusElem, timeout} = options;
|
||||||
if (difficulty === 0) {
|
if (difficulty === 0) {
|
||||||
return Promise.resolve(evt);
|
return Promise.resolve({
|
||||||
|
id: getEventHash(evt),
|
||||||
|
...evt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const cancelBtn = elem('button', {className: 'btn-inline'}, [elem('small', {}, 'cancel')]);
|
const cancelBtn = elem('button', {className: 'btn-inline'}, [elem('small', {}, 'cancel')]);
|
||||||
statusElem.replaceChildren('working…', cancelBtn);
|
statusElem.replaceChildren('working…', cancelBtn);
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import {Event, Filter, relayInit, Relay, Sub} from 'nostr-tools';
|
||||||
|
|
||||||
|
type SubCallback = (
|
||||||
|
event: Readonly<Event>,
|
||||||
|
relay: Readonly<string>,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
type Subscribe = {
|
||||||
|
cb: SubCallback;
|
||||||
|
filter: Filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
const relayList: Array<Relay> = [];
|
||||||
|
const subList: Array<Sub> = [];
|
||||||
|
const currentSubList: Array<Subscribe> = [];
|
||||||
|
|
||||||
|
export const addRelay = async (url: string) => {
|
||||||
|
const relay = relayInit(url);
|
||||||
|
relay.on('connect', () => {
|
||||||
|
console.info(`connected to ${relay.url}`);
|
||||||
|
});
|
||||||
|
relay.on('error', () => {
|
||||||
|
console.warn(`failed to connect to ${relay.url}`);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await relay.connect();
|
||||||
|
currentSubList.forEach(({cb, filter}) => subscribe(cb, filter, relay));
|
||||||
|
relayList.push(relay);
|
||||||
|
} catch {
|
||||||
|
console.warn(`could not connect to ${url}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribe = (sub: Sub) => {
|
||||||
|
sub.unsub();
|
||||||
|
subList.splice(subList.indexOf(sub), 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscribe = (
|
||||||
|
cb: SubCallback,
|
||||||
|
filter: Filter,
|
||||||
|
relay: Relay,
|
||||||
|
) => {
|
||||||
|
const sub = relay.sub([filter]);
|
||||||
|
subList.push(sub);
|
||||||
|
sub.on('event', (event: Event) => {
|
||||||
|
cb(event, relay.url);
|
||||||
|
});
|
||||||
|
sub.on('eose', () => {
|
||||||
|
// console.log('eose', relay.url);
|
||||||
|
// unsubscribe(sub);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscribeAll = (
|
||||||
|
cb: SubCallback,
|
||||||
|
filter: Filter,
|
||||||
|
) => {
|
||||||
|
relayList.forEach(relay => subscribe(cb, filter, relay));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sub = (obj: Subscribe) => {
|
||||||
|
currentSubList.push(obj);
|
||||||
|
subscribeAll(obj.cb, obj.filter);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unsubAll = () => {
|
||||||
|
subList.forEach(unsubscribe);
|
||||||
|
currentSubList.length = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PublishCallback = (
|
||||||
|
relay: string,
|
||||||
|
errorMessage?: string,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export const publish = (
|
||||||
|
event: Event,
|
||||||
|
cb: PublishCallback,
|
||||||
|
) => {
|
||||||
|
relayList.forEach(relay => {
|
||||||
|
const pub = relay.publish(event);
|
||||||
|
pub.on('ok', () => {
|
||||||
|
console.info(`${relay.url} has accepted our event`);
|
||||||
|
cb(relay.url);
|
||||||
|
});
|
||||||
|
pub.on('failed', (reason: any) => {
|
||||||
|
console.error(`failed to publish to ${relay.url}: ${reason}`);
|
||||||
|
cb(relay.url, reason);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
addRelay('wss://relay.snort.social'); // good one
|
||||||
|
addRelay('wss://nostr.bitcoiner.social');
|
||||||
|
addRelay('wss://nostr.mom');
|
||||||
|
addRelay('wss://relay.nostr.bg');
|
||||||
|
addRelay('wss://nos.lol');
|
||||||
|
addRelay('wss://relay.nostr.ch');
|
|
@ -26,7 +26,7 @@ function mine(event, difficulty, timeout = 5) {
|
||||||
const id = getEventHash(event);
|
const id = getEventHash(event);
|
||||||
if (zeroLeadingBitsCount(id) === difficulty) {
|
if (zeroLeadingBitsCount(id) === difficulty) {
|
||||||
console.timeEnd('pow');
|
console.timeEnd('pow');
|
||||||
return event;
|
return {id, ...event};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue