@graperank/protocols-nostr
Version:
The Nostr Protocols plugin for GrapeRank allows the Interpreter to fetch and normalize ratings data from any kind[s] of Nostr event.
306 lines (305 loc) • 14 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NostrProtocol = void 0;
exports.applyRatingsByTag = applyRatingsByTag;
exports.validatePubkey = validatePubkey;
exports.validateEachEventHasAuthor = validateEachEventHasAuthor;
exports.validateOneEventIsNew = validateOneEventIsNew;
exports.getEventsAuthors = getEventsAuthors;
const pool_1 = require("nostr-tools/pool");
const pool_2 = require("nostr-tools/pool");
const util_1 = require("@graperank/util");
const ws_1 = __importDefault(require("ws"));
const nip19_1 = require("nostr-tools/nip19");
(0, pool_2.useWebSocketImplementation)(ws_1.default);
const relays = [
"wss://gv.rogue.earth",
// "wss://purplepag.es",
// "wss://relay.primal.net",
// "wss://relay.damus.io",
// "wss://nostr-pub.wellorder.net",
// "wss://relay.nostr.bg",
"wss://nostr.bitcoiner.social",
// "wss://nostr.fmt.wiz.biz",
// "wss://nostr.oxtr.dev",
// "wss://nostr.mom",
"wss://relay.nostr.band",
"wss://relay.snort.social",
// "wss://soloco.nl",
"wss://nos.lol",
];
const maxauthors = 1000;
const delaybetweenfetches = 500; // milliceconds
class NostrProtocol {
kinds;
request;
_params;
get params() { return { ...this._params, ...this.request.params }; }
fetched = [];
interpreted = new Map();
interpret;
validate;
constructor(config) {
this.kinds = config.kinds;
this._params = config.params;
this.validate = config.validate;
this.interpret = async (dos) => {
if (!this.fetched.length)
throw ('GrapeRank : ' + this.request.protocol + ' protocol interpret() : ERROR : NO EVENTS FETCHED PRIOR TO INTERPRET');
// use the set of fetched events at fetchedIndex or LAST index
dos = dos || this.fetched.length;
let fetchedIndex = dos - 1;
let newratings;
console.log("GrapeRank : ", this.request.protocol, " protocol : interptreting ", this.fetched[fetchedIndex].size, " events fetched in iteration ", dos);
// interpret newratings via defined callback or default
if (config.interpret) {
newratings = await config.interpret(this, dos);
}
else {
newratings = await applyRatingsByTag(this, dos);
}
// merge newratings into this.interpreted
let numratingsmerged = 0;
let numraterratings = 0;
let numratingsduplicate = 0;
newratings.forEach((rateemap, rater) => {
let raterratings = this.interpreted.get(rater);
if (raterratings) {
numraterratings = raterratings.size;
rateemap.forEach((ratingdata, ratee) => {
raterratings.set(ratee, ratingdata);
});
numratingsmerged = numratingsmerged + (raterratings.size - numraterratings);
numratingsduplicate = numratingsduplicate + (rateemap.size - (raterratings.size - numraterratings));
}
else {
numratingsmerged = numratingsmerged + rateemap.size;
this.interpreted.set(rater, rateemap);
}
});
console.log("GrapeRank : ", this.request.protocol, " protocol : merged iteration ", dos, " into total interpreted : ", numratingsmerged, " new ratings and ", numratingsduplicate, " duplicate ratings from ", newratings.size, " authors");
return newratings;
};
}
// breaks up a large raters list into multiple authors lists
// suitable for relay requests, and sends them all in parrallel
// returns a single promise that is resolved when all fetches are complete.
async fetchData(authors) {
authors = authors || new Set(this.request.authors);
// authorslists is authors broken into an array of list,
// where each list is maximum size allowed for relay requests
const authorslists = authors.size > maxauthors ?
await (0, util_1.sliceBigArray)([...authors], maxauthors) : [[...authors]];
// fetchedSet is where each promise will add newly fetched events
const fetchedSet = new Set();
const promises = [];
let promise;
console.log("GrapeRank : nostr protocol : fetching events in ", authorslists.length, " requests for ", authors.size, " raters");
// send one relay request per `maxauthors` sized list of authors
for (let index in authorslists) {
let fetchfilter = {
...this.request.filter,
authors: authorslists[index],
kinds: this.kinds,
};
// delay between relay requests
await new Promise((resolve) => {
setTimeout(() => resolve(), delaybetweenfetches);
});
promises.push(this.fetchEventsPromise(fetchfilter, fetchedSet, index));
}
// wait for all promises to resolve
await Promise.all(promises);
console.log("GrapeRank : nostr protocol : fetching complete with ", fetchedSet.size, " events ");
// add fetchedSet to this.fetched array of event sets,
// and return the new dos for ratings interpretation
return this.fetched.push(fetchedSet);
}
// An iterative function ...
// possibly calls itself again if validation fails
// returns a single promise with possibly nested promises
async fetchEventsPromise(filter, fetchedSet, iteration = 0) {
if (!filter.authors)
return new Promise((resolve, reject) => reject("No authors provided"));
console.log("GrapeRank : nostr protocol : fetching events in request ", iteration, " for ", filter.authors?.length, " raters");
return new Promise((resolve) => {
fetchEvents(filter).then(async (newFetchedSet) => {
let validation = true; // this.validate ? this.validate(newFetchedSet, filter.authors as string[], fetchedSet) : true
try {
// FALSE validation will log error
// if(validation === false ) {
// throw('events validaiton failed')
// }
// TRUE validation will add events to fetchedSet
if (validation === true) {
// fetchedSet = new Set([...fetchedSet, ...newFetchedSet])
newFetchedSet.forEach((event) => { fetchedSet.add(event); });
throw ("fetched " + newFetchedSet.size + " new events, for total of " + fetchedSet.size);
}
// otherwise try request again with reduced authors list
console.log("GrapeRank : nostr protocol : fetch request ", iteration, " : requesting again");
await this.fetchEventsPromise({ ...filter, authors: validation }, fetchedSet, iteration);
}
catch (e) {
console.log("GrapeRank : nostr protocol : fetch request ", iteration, " complete : ", e);
}
resolve();
}).catch((error) => {
console.log("GrapeRank : nostr protocol : ERROR in fetch request ", iteration, error);
});
});
}
}
exports.NostrProtocol = NostrProtocol;
async function applyRatingsByTag(instance, dos, tag = "p", rateeindex = 1, scoreindex) {
console.log("GrapeRank : nostr protocol : applyRatingsByTag()");
let fetchedIndex = dos - 1;
const fetchedSet = instance.fetched[fetchedIndex];
const newratingsmap = new Map();
let eventindex = 0, totalratings = 0, duplicateratings = 0, rater, ratee,
// apply a single score for all ratings, as indicated in params.score
defaultscore = instance.params.score || 0, defaultconfidence = instance.params.confidence || .5;
// loop through the events of fetchedSet to find tags for making new ratings
for (let event of fetchedSet) {
rater = event.pubkey;
const raterratings = new Map;
eventindex++;
let oldduplicateratings = duplicateratings;
// console.log("GrapeRank : applyRatingsByTag : proccessing event "+eventindex+"/"+numevents+" with " + event.tags.length + " tags")
// let eventratings : PartialRatingsList = []
// loop through all tags of each event to find the ones to make ratings from
// TODO iterate if more than 10000 tags in this event
if (!!event.tags && event.tags.length < 10000) {
for (let t in event.tags) {
// `rateeindex` argument determines the tag index from which to get the ID of what's been rated
ratee = event.tags[t][rateeindex];
// skip for this tag if rating already exists for this rater / ratee
if (raterratings.has(ratee)) {
duplicateratings++;
continue;
}
// `tag` argument defines what event tag to makes ratings from
if (event.tags[t][0] == tag) {
// validate pubkey before applying a rating
if (tag == 'p' && rateeindex == 1 && !validatePubkey(event.tags[t][1])) {
continue;
}
raterratings.set(ratee, {
confidence: defaultconfidence,
// if `scoreindex` argument has been defined...
// and if the value of this tag[scoreindex] is a property in params ...
// then apply a custom score per rating according to the index value in params
score: scoreindex ? instance.params[event.tags[t][scoreindex]] : defaultscore,
dos: dos
});
if (ratee == util_1.DEBUGTARGET)
console.log('DEBUGTARGET : nostr-protocol : interpreted rating for target : ', raterratings.get(util_1.DEBUGTARGET));
totalratings++;
}
}
// console.log("GrapeRank : applyRatingsByTag : event by author proccessed with ", raterratings.size, " new ratings tags and ", duplicateratings - oldduplicateratings, " ratings already interpreted" )
}
else {
console.log("GrapeRank : nostr protocol : applyRatingsByTag : event not processed");
}
// handle big arrays with care
// if(eventratings) ratings = await mergeBigArrays(ratings, eventratings, 10000)
newratingsmap.set(rater, raterratings);
}
console.log("GrapeRank : nostr protocol : applyRatingsByTag : total interpreted ", totalratings, " new ratings and skipped ", duplicateratings, " duplicate ratings for ", newratingsmap.size, " authors in iteration ", fetchedIndex);
return newratingsmap;
}
function validatePubkey(pubkey) {
try {
(0, nip19_1.npubEncode)(pubkey);
}
catch (e) {
return false;
}
return true;
}
function validateEachEventHasAuthor(events, authors, previous) {
if (authors.length == events.size)
return true;
if (!validateOneEventIsNew(events, authors, previous))
return false;
let authorswithoutevents = getEventsAuthors(events, authors);
return authorswithoutevents.length ? authorswithoutevents : true;
}
function validateOneEventIsNew(events, authors, previous) {
if (!previous || !previous.size)
return true;
previous.forEach((pevent) => {
events.forEach((newevent) => {
if (pevent.id != newevent.id)
return true;
});
});
return false;
}
function getEventsAuthors(events, exclude) {
const authors = [];
events.forEach((event) => {
if (!exclude || !exclude.includes(event.pubkey))
authors.push(event.pubkey);
});
return authors;
}
/**
* Adapted from NDK
* @param filters
* @returns
*/
async function fetchEvents(filters) {
filters = filters instanceof Array ? filters : [filters];
return new Promise((resolve) => {
const events = new Map();
// console.log("GrapeRank : nostr protocol : fetchEvents() : calling fetch() for ", )
const onEvent = (event) => {
// console.log("GrapeRank : nostr : fetchEvents() : recieved kind-"+event.kind, event.id)
// if (!(event instanceof NostrEvent)) event = new NostrEvent(undefined, event);
const dedupKey = deduplicationKey(event);
const existingEvent = events.get(dedupKey);
if (existingEvent) {
event = dedupEvent(existingEvent, event);
}
// event.ndk = ndk;
events.set(dedupKey, event);
};
const pool = new pool_1.SimplePool();
let h = pool.subscribeMany([...relays], filters, {
onevent: onEvent,
oneose() {
h.close();
resolve(new Set(events.values()));
}
});
});
}
function dedupEvent(event1, event2) {
// return the newest of the two
if (event1.created_at > event2.created_at) {
return event1;
}
return event2;
}
/**
* Provides a deduplication key for the event.
*
* For kinds 0, 3, 10k-20k this will be the event <kind>:<pubkey>
* For kinds 30k-40k this will be the event <kind>:<pubkey>:<d-tag>
* For all other kinds this will be the event id
*/
function deduplicationKey(event) {
if (event.kind === 0 ||
event.kind === 3 ||
(event.kind && event.kind >= 10000 && event.kind < 20000)) {
return `${event.kind}:${event.pubkey}`;
}
else {
return event.id;
}
}