Init2
This commit is contained in:
96
src/nostr/Connection.js
Normal file
96
src/nostr/Connection.js
Normal file
@ -0,0 +1,96 @@
|
||||
import { Subscriptions } from "./Subscriptions";
|
||||
import Event from "./Event";
|
||||
|
||||
export default class Connection {
|
||||
constructor(addr) {
|
||||
this.Address = addr;
|
||||
this.Socket = new WebSocket(addr);
|
||||
this.Socket.onopen = (e) => this.OnOpen(e);
|
||||
this.Socket.onmessage = (e) => this.OnMessage(e);
|
||||
this.Socket.onerror = (e) => this.OnError(e);
|
||||
this.Pending = [];
|
||||
this.Subscriptions = {};
|
||||
}
|
||||
|
||||
OnOpen(e) {
|
||||
console.log(`Opened connection to: ${this.Address}`);
|
||||
console.log(e);
|
||||
|
||||
// send pending
|
||||
for (let p of this.Pending) {
|
||||
this._SendJson(p);
|
||||
}
|
||||
}
|
||||
|
||||
OnMessage(e) {
|
||||
let msg = JSON.parse(e.data);
|
||||
let tag = msg[0];
|
||||
switch (tag) {
|
||||
case "EVENT": {
|
||||
this._OnEvent(msg[1], msg[2]);
|
||||
break;
|
||||
}
|
||||
case "EOSE": {
|
||||
// ignored for now
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn(`Unknown tag: ${tag}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnError(e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
SendEvent(e) {
|
||||
let req = ["EVENT", e];
|
||||
this._SendJson(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to data from this connection
|
||||
* @param {Subscriptions | Array<Subscriptions>} sub Subscriptions object
|
||||
*/
|
||||
AddSubscription(sub) {
|
||||
let req = ["REQ", sub.Id, sub.ToObject()];
|
||||
if(sub.OrSubs.length > 0) {
|
||||
req = [
|
||||
...req,
|
||||
...sub.OrSubs.map(o => o.ToObject())
|
||||
];
|
||||
}
|
||||
this._SendJson(req);
|
||||
this.Subscriptions[sub.Id] = sub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a subscription
|
||||
* @param {any} subId Subscription id to remove
|
||||
*/
|
||||
RemoveSubscription(subId) {
|
||||
let req = ["CLOSE", subId];
|
||||
this._SendJson(req);
|
||||
delete this.Subscriptions[subId];
|
||||
}
|
||||
|
||||
_SendJson(obj) {
|
||||
if (this.Socket.readyState !== this.Socket.OPEN) {
|
||||
this.Pending.push(obj);
|
||||
return;
|
||||
}
|
||||
let json = JSON.stringify(obj);
|
||||
console.debug(`[${this.Address}] >> ${json}`);
|
||||
this.Socket.send(json);
|
||||
}
|
||||
|
||||
_OnEvent(subId, ev) {
|
||||
if (this.Subscriptions[subId]) {
|
||||
this.Subscriptions[subId].OnEvent(ev);
|
||||
} else {
|
||||
console.warn("No subscription for event!");
|
||||
}
|
||||
}
|
||||
}
|
138
src/nostr/Event.js
Normal file
138
src/nostr/Event.js
Normal file
@ -0,0 +1,138 @@
|
||||
import * as secp from '@noble/secp256k1';
|
||||
import EventKind from "./EventKind";
|
||||
import Tag from './Tag';
|
||||
import Thread from './Thread';
|
||||
|
||||
export default class Event {
|
||||
constructor() {
|
||||
/**
|
||||
* Id of the event
|
||||
* @type {string}
|
||||
*/
|
||||
this.Id = null;
|
||||
|
||||
/**
|
||||
* Pub key of the creator
|
||||
* @type {string}
|
||||
*/
|
||||
this.PubKey = null;
|
||||
|
||||
/**
|
||||
* Timestamp when the event was created
|
||||
* @type {number}
|
||||
*/
|
||||
this.CreatedAt = null;
|
||||
|
||||
/**
|
||||
* The type of event
|
||||
* @type {EventKind}
|
||||
*/
|
||||
this.Kind = null;
|
||||
|
||||
/**
|
||||
* A list of metadata tags
|
||||
* @type {Array<Tag>}
|
||||
*/
|
||||
this.Tags = [];
|
||||
|
||||
/**
|
||||
* Content of the event
|
||||
* @type {string}
|
||||
*/
|
||||
this.Content = null;
|
||||
|
||||
/**
|
||||
* Signature of this event from the creator
|
||||
* @type {string}
|
||||
*/
|
||||
this.Signature = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign this message with a private key
|
||||
* @param {string} key Key to sign message with
|
||||
*/
|
||||
async Sign(key) {
|
||||
this.Id = await this.CreateId();
|
||||
|
||||
let sig = await secp.schnorr.sign(this.Id, key);
|
||||
this.Signature = secp.utils.bytesToHex(sig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the signature of this message
|
||||
* @returns True if valid signature
|
||||
*/
|
||||
async Verify() {
|
||||
let id = await this.CreateId();
|
||||
let result = await secp.schnorr.verify(this.Signature, id, this.PubKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
async CreateId() {
|
||||
let payload = [
|
||||
0,
|
||||
this.PubKey,
|
||||
this.CreatedAt,
|
||||
this.Kind,
|
||||
this.Tags.map(a => a.ToObject()),
|
||||
this.Content
|
||||
];
|
||||
|
||||
let payloadData = new TextEncoder().encode(JSON.stringify(payload));
|
||||
let data = await secp.utils.sha256(payloadData);
|
||||
let hash = secp.utils.bytesToHex(data);
|
||||
if(this.Id !== null && hash !== this.Id) {
|
||||
console.debug(payload);
|
||||
throw "ID doesnt match!";
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get thread information
|
||||
* @returns {Thread}
|
||||
*/
|
||||
GetThread() {
|
||||
return Thread.ExtractThread(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this event have content
|
||||
* @returns {boolean}
|
||||
*/
|
||||
IsContent() {
|
||||
const ContentKinds = [
|
||||
EventKind.TextNote
|
||||
];
|
||||
return ContentKinds.includes(this.Kind);
|
||||
}
|
||||
|
||||
static FromObject(obj) {
|
||||
if(typeof obj !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let ret = new Event();
|
||||
ret.Id = obj.id;
|
||||
ret.PubKey = obj.pubkey;
|
||||
ret.CreatedAt = obj.created_at;
|
||||
ret.Kind = obj.kind;
|
||||
ret.Tags = obj.tags.map(e => new Tag(e));
|
||||
ret.Content = obj.content;
|
||||
ret.Signature = obj.sig;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ToObject() {
|
||||
return {
|
||||
id: this.Id,
|
||||
pubkey: this.PubKey,
|
||||
created_at: this.CreatedAt,
|
||||
kind: this.Kind,
|
||||
tags: this.Tags.map(a => a.ToObject()),
|
||||
content: this.Content,
|
||||
sig: this.Signature
|
||||
};
|
||||
}
|
||||
}
|
11
src/nostr/EventKind.js
Normal file
11
src/nostr/EventKind.js
Normal file
@ -0,0 +1,11 @@
|
||||
const EventKind = {
|
||||
Unknown: -1,
|
||||
SetMetadata: 0,
|
||||
TextNote: 1,
|
||||
RecommendServer: 2,
|
||||
ContactList: 3, // NIP-02
|
||||
DirectMessage: 4, // NIP-04
|
||||
Reaction: 7 // NIP-25
|
||||
};
|
||||
|
||||
export default EventKind;
|
110
src/nostr/Subscriptions.js
Normal file
110
src/nostr/Subscriptions.js
Normal file
@ -0,0 +1,110 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
export class Subscriptions {
|
||||
constructor() {
|
||||
/**
|
||||
* A unique id for this subscription filter
|
||||
*/
|
||||
this.Id = uuid();
|
||||
|
||||
/**
|
||||
* a list of event ids or prefixes
|
||||
*/
|
||||
this.Ids = new Set();
|
||||
|
||||
/**
|
||||
* a list of pubkeys or prefixes, the pubkey of an event must be one of these
|
||||
*/
|
||||
this.Authors = new Set();
|
||||
|
||||
/**
|
||||
* a list of a kind numbers
|
||||
*/
|
||||
this.Kinds = new Set();
|
||||
|
||||
/**
|
||||
* a list of event ids that are referenced in an "e" tag
|
||||
*/
|
||||
this.ETags = new Set();
|
||||
|
||||
/**
|
||||
* a list of pubkeys that are referenced in a "p" tag
|
||||
*/
|
||||
this.PTags = new Set();
|
||||
|
||||
/**
|
||||
* a timestamp, events must be newer than this to pass
|
||||
*/
|
||||
this.Since = NaN;
|
||||
|
||||
/**
|
||||
* a timestamp, events must be older than this to pass
|
||||
*/
|
||||
this.Until = NaN;
|
||||
|
||||
/**
|
||||
* maximum number of events to be returned in the initial query
|
||||
*/
|
||||
this.Limit = NaN;
|
||||
|
||||
/**
|
||||
* Handler function for this event
|
||||
*/
|
||||
this.OnEvent = (e) => { console.warn(`No event handler was set on subscription: ${this.Id}`) };
|
||||
|
||||
/**
|
||||
* Collection of OR sub scriptions linked to this
|
||||
*/
|
||||
this.OrSubs = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds OR filter subscriptions
|
||||
* @param {Subscriptions} sub Extra filters
|
||||
*/
|
||||
AddSubscription(sub) {
|
||||
this.OrSubs.push(sub);
|
||||
}
|
||||
|
||||
static FromObject(obj) {
|
||||
let ret = new Subscriptions();
|
||||
ret.Ids = new Set(obj.ids);
|
||||
ret.Authors = new Set(obj.authors);
|
||||
ret.Kinds = new Set(obj.kinds);
|
||||
ret.ETags = new Set(obj["#e"]);
|
||||
ret.PTags = new Set(obj["#p"]);
|
||||
ret.Since = parseInt(obj.since);
|
||||
ret.Until = parseInt(obj.until);
|
||||
ret.Limit = parseInt(obj.limit);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ToObject() {
|
||||
let ret = {};
|
||||
if (this.Ids.size > 0) {
|
||||
ret.ids = Array.from(this.Ids);
|
||||
}
|
||||
if (this.Authors.size > 0) {
|
||||
ret.authors = Array.from(this.Authors);
|
||||
}
|
||||
if (this.Kinds.size > 0) {
|
||||
ret.kinds = Array.from(this.Kinds);
|
||||
}
|
||||
if (this.ETags.size > 0) {
|
||||
ret["#e"] = Array.from(this.ETags);
|
||||
}
|
||||
if (this.PTags.size > 0) {
|
||||
ret["#p"] = Array.from(this.PTags);
|
||||
}
|
||||
if (!isNaN(this.Since)) {
|
||||
ret.since = this.Since;
|
||||
}
|
||||
if (!isNaN(this.Until)) {
|
||||
ret.until = this.Until;
|
||||
}
|
||||
if (!isNaN(this.Limit)) {
|
||||
ret.limit = this.Limit;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
46
src/nostr/System.js
Normal file
46
src/nostr/System.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import Connection from "./Connection";
|
||||
|
||||
/**
|
||||
* Manages nostr content retrival system
|
||||
*/
|
||||
export class NostrSystem {
|
||||
constructor() {
|
||||
this.Sockets = {};
|
||||
this.Subscriptions = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a NOSTR relay if not already connected
|
||||
* @param {string} address
|
||||
*/
|
||||
ConnectToRelay(address) {
|
||||
if(typeof this.Sockets[address] === "undefined") {
|
||||
let c = new Connection(address);
|
||||
for(let s of Object.values(this.Subscriptions)) {
|
||||
c.AddSubscription(s);
|
||||
}
|
||||
this.Sockets[address] = c;
|
||||
}
|
||||
}
|
||||
|
||||
AddSubscription(sub) {
|
||||
for(let s of Object.values(this.Sockets)) {
|
||||
s.AddSubscription(sub);
|
||||
}
|
||||
this.Subscriptions[sub.Id] = sub;
|
||||
}
|
||||
|
||||
RemoveSubscription(subId) {
|
||||
for(let s of Object.values(this.Sockets)) {
|
||||
s.RemoveSubscription(subId);
|
||||
}
|
||||
delete this.Subscriptions[subId];
|
||||
}
|
||||
|
||||
BroadcastEvent(ev) {
|
||||
for(let s of Object.values(this.Sockets)) {
|
||||
s.SendEvent(ev);
|
||||
}
|
||||
}
|
||||
}
|
36
src/nostr/Tag.js
Normal file
36
src/nostr/Tag.js
Normal file
@ -0,0 +1,36 @@
|
||||
export default class Tag {
|
||||
constructor(tag) {
|
||||
this.Key = tag[0];
|
||||
this.Event = null;
|
||||
this.PubKey = null;
|
||||
this.Relay = null;
|
||||
this.Marker = null;
|
||||
|
||||
switch (this.Key) {
|
||||
case "e": {
|
||||
// ["e", <event-id>, <relay-url>, <marker>]
|
||||
this.Event = tag[1];
|
||||
this.Relay = tag.length > 2 ? tag[2] : null;
|
||||
this.Marker = tag.length > 3 ? tag[3] : null;
|
||||
break;
|
||||
}
|
||||
case "p": {
|
||||
// ["p", <pubkey>]
|
||||
this.PubKey = tag[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToObject() {
|
||||
switch(this.Key) {
|
||||
case "e": {
|
||||
return ["e", this.Event, this.Relay, this.Marker].filter(a => a !== null);
|
||||
}
|
||||
case "p": {
|
||||
return ["p", this.PubKey];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
40
src/nostr/Thread.js
Normal file
40
src/nostr/Thread.js
Normal file
@ -0,0 +1,40 @@
|
||||
import Event from "./Event";
|
||||
|
||||
export default class Thread {
|
||||
constructor() {
|
||||
this.Root = null;
|
||||
this.ReplyTo = null;
|
||||
this.Mentions = [];
|
||||
this.Reply = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract thread information from an Event
|
||||
* @param {Event} ev Event to extract thread from
|
||||
*/
|
||||
static ExtractThread(ev) {
|
||||
let isThread = ev.Tags.some(a => a.Key === "e");
|
||||
if (!isThread) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let ret = new Thread();
|
||||
ret.Reply = ev;
|
||||
let eTags = ev.Tags.filter(a => a.Key === "e");
|
||||
let marked = eTags.some(a => a.Marker !== null);
|
||||
if (!marked) {
|
||||
ret.Root = eTags[0];
|
||||
if (eTags.length > 2) {
|
||||
ret.Mentions = eTags.slice(1, -1);
|
||||
}
|
||||
ret.ReplyTo = eTags[eTags.length - 1];
|
||||
} else {
|
||||
let root = eTags.find(a => a.Marker === "root");
|
||||
let reply = eTags.find(a => a.Marker === "reply");
|
||||
ret.Root = root;
|
||||
ret.ReplyTo = reply;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user