refactor: RequestBuilder
This commit is contained in:
@ -1,25 +1,18 @@
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { Subscriptions } from "./Subscriptions";
|
||||
import { default as NEvent } from "./Event";
|
||||
import { DefaultConnectTimeout } from "./Const";
|
||||
import { ConnectionStats } from "./ConnectionStats";
|
||||
import { RawEvent, RawReqFilter, TaggedRawEvent, u256 } from "./index";
|
||||
import { RawEvent, RawReqFilter, ReqCommand, TaggedRawEvent, u256 } from "./index";
|
||||
import { RelayInfo } from "./RelayInfo";
|
||||
import Nips from "./Nips";
|
||||
import { unwrap } from "./Util";
|
||||
|
||||
export type CustomHook = (state: Readonly<StateSnapshot>) => void;
|
||||
export type AuthHandler = (
|
||||
challenge: string,
|
||||
relay: string
|
||||
) => Promise<NEvent | undefined>;
|
||||
export type AuthHandler = (challenge: string, relay: string) => Promise<RawEvent | undefined>;
|
||||
|
||||
/**
|
||||
* Relay settings
|
||||
*/
|
||||
export type RelaySettings = {
|
||||
export interface RelaySettings {
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
};
|
||||
@ -42,35 +35,36 @@ export type StateSnapshot = {
|
||||
export class Connection {
|
||||
Id: string;
|
||||
Address: string;
|
||||
Socket: WebSocket | null;
|
||||
Pending: Array<RawReqFilter>;
|
||||
Subscriptions: Map<string, Subscriptions>;
|
||||
Socket: WebSocket | null = null;
|
||||
|
||||
PendingRaw: Array<object> = [];
|
||||
PendingRequests: Array<ReqCommand> = [];
|
||||
ActiveRequests: Set<string> = new Set();
|
||||
|
||||
Settings: RelaySettings;
|
||||
Info?: RelayInfo;
|
||||
ConnectTimeout: number;
|
||||
Stats: ConnectionStats;
|
||||
StateHooks: Map<string, CustomHook>;
|
||||
HasStateChange: boolean;
|
||||
ConnectTimeout: number = DefaultConnectTimeout;
|
||||
Stats: ConnectionStats = new ConnectionStats();
|
||||
StateHooks: Map<string, CustomHook> = new Map();
|
||||
HasStateChange: boolean = true;
|
||||
CurrentState: StateSnapshot;
|
||||
LastState: Readonly<StateSnapshot>;
|
||||
IsClosed: boolean;
|
||||
ReconnectTimer: ReturnType<typeof setTimeout> | null;
|
||||
EventsCallback: Map<u256, (msg: boolean[]) => void>;
|
||||
OnConnected?: () => void;
|
||||
OnEvent?: (sub: string, e: TaggedRawEvent) => void;
|
||||
OnEose?: (sub: string) => void;
|
||||
Auth?: AuthHandler;
|
||||
AwaitingAuth: Map<string, boolean>;
|
||||
Authed: boolean;
|
||||
Ephemeral: boolean;
|
||||
EphemeralTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
constructor(addr: string, options: RelaySettings, auth?: AuthHandler) {
|
||||
constructor(addr: string, options: RelaySettings, auth?: AuthHandler, ephemeral: boolean = false) {
|
||||
this.Id = uuid();
|
||||
this.Address = addr;
|
||||
this.Socket = null;
|
||||
this.Pending = [];
|
||||
this.Subscriptions = new Map();
|
||||
this.Settings = options;
|
||||
this.ConnectTimeout = DefaultConnectTimeout;
|
||||
this.Stats = new ConnectionStats();
|
||||
this.StateHooks = new Map();
|
||||
this.HasStateChange = true;
|
||||
this.CurrentState = {
|
||||
connected: false,
|
||||
disconnects: 0,
|
||||
@ -87,13 +81,29 @@ export class Connection {
|
||||
this.AwaitingAuth = new Map();
|
||||
this.Authed = false;
|
||||
this.Auth = auth;
|
||||
this.Ephemeral = ephemeral;
|
||||
|
||||
if (this.Ephemeral) {
|
||||
this.ResetEphemeralTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
ResetEphemeralTimeout() {
|
||||
if (this.EphemeralTimeout) {
|
||||
clearTimeout(this.EphemeralTimeout);
|
||||
}
|
||||
if (this.Ephemeral) {
|
||||
this.EphemeralTimeout = setTimeout(() => {
|
||||
this.Close();
|
||||
}, 10_000);
|
||||
}
|
||||
}
|
||||
|
||||
async Connect() {
|
||||
try {
|
||||
if (this.Info === undefined) {
|
||||
const u = new URL(this.Address);
|
||||
const rsp = await fetch(`https://${u.host}`, {
|
||||
const rsp = await fetch(`${u.protocol === "wss:" ? "https:" : "http:"}//${u.host}`, {
|
||||
headers: {
|
||||
accept: "application/nostr+json",
|
||||
},
|
||||
@ -101,7 +111,7 @@ export class Connection {
|
||||
if (rsp.ok) {
|
||||
const data = await rsp.json();
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (v === "unset" || v === "") {
|
||||
if (v === "unset" || v === "" || v === "~") {
|
||||
data[k] = undefined;
|
||||
}
|
||||
}
|
||||
@ -112,11 +122,6 @@ export class Connection {
|
||||
console.warn("Could not load relay information", e);
|
||||
}
|
||||
|
||||
if (this.IsClosed) {
|
||||
this._UpdateState();
|
||||
return;
|
||||
}
|
||||
|
||||
this.IsClosed = false;
|
||||
this.Socket = new WebSocket(this.Address);
|
||||
this.Socket.onopen = () => this.OnOpen();
|
||||
@ -132,17 +137,18 @@ export class Connection {
|
||||
this.ReconnectTimer = null;
|
||||
}
|
||||
this.Socket?.close();
|
||||
this._UpdateState();
|
||||
this.#UpdateState();
|
||||
}
|
||||
|
||||
OnOpen() {
|
||||
this.ConnectTimeout = DefaultConnectTimeout;
|
||||
this._InitSubscriptions();
|
||||
console.log(`[${this.Address}] Open!`);
|
||||
this.OnConnected?.();
|
||||
}
|
||||
|
||||
OnClose(e: CloseEvent) {
|
||||
if (!this.IsClosed) {
|
||||
this.#ResetQueues();
|
||||
this.ConnectTimeout = this.ConnectTimeout * 2;
|
||||
console.log(
|
||||
`[${this.Address}] Closed (${e.reason}), trying again in ${(
|
||||
@ -159,7 +165,7 @@ export class Connection {
|
||||
console.log(`[${this.Address}] Closed!`);
|
||||
this.ReconnectTimer = null;
|
||||
}
|
||||
this._UpdateState();
|
||||
this.#UpdateState();
|
||||
}
|
||||
|
||||
OnMessage(e: MessageEvent) {
|
||||
@ -170,17 +176,17 @@ export class Connection {
|
||||
case "AUTH": {
|
||||
this._OnAuthAsync(msg[1]);
|
||||
this.Stats.EventsReceived++;
|
||||
this._UpdateState();
|
||||
this.#UpdateState();
|
||||
break;
|
||||
}
|
||||
case "EVENT": {
|
||||
this._OnEvent(msg[1], msg[2]);
|
||||
this.OnEvent?.(msg[1], msg[2]);
|
||||
this.Stats.EventsReceived++;
|
||||
this._UpdateState();
|
||||
this.#UpdateState();
|
||||
break;
|
||||
}
|
||||
case "EOSE": {
|
||||
this._OnEnd(msg[1]);
|
||||
this.OnEose?.(msg[1]);
|
||||
break;
|
||||
}
|
||||
case "OK": {
|
||||
@ -208,26 +214,26 @@ export class Connection {
|
||||
|
||||
OnError(e: Event) {
|
||||
console.error(e);
|
||||
this._UpdateState();
|
||||
this.#UpdateState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send event on this connection
|
||||
*/
|
||||
SendEvent(e: NEvent) {
|
||||
SendEvent(e: RawEvent) {
|
||||
if (!this.Settings.write) {
|
||||
return;
|
||||
}
|
||||
const req = ["EVENT", e.ToObject()];
|
||||
this._SendJson(req);
|
||||
const req = ["EVENT", e];
|
||||
this.#SendJson(req);
|
||||
this.Stats.EventsSent++;
|
||||
this._UpdateState();
|
||||
this.#UpdateState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send event on this connection and wait for OK response
|
||||
*/
|
||||
async SendAsync(e: NEvent, timeout = 5000) {
|
||||
async SendAsync(e: RawEvent, timeout = 5000) {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (!this.Settings.write) {
|
||||
resolve();
|
||||
@ -236,53 +242,18 @@ export class Connection {
|
||||
const t = setTimeout(() => {
|
||||
resolve();
|
||||
}, timeout);
|
||||
this.EventsCallback.set(e.Id, () => {
|
||||
this.EventsCallback.set(e.id, () => {
|
||||
clearTimeout(t);
|
||||
resolve();
|
||||
});
|
||||
|
||||
const req = ["EVENT", e.ToObject()];
|
||||
this._SendJson(req);
|
||||
const req = ["EVENT", e];
|
||||
this.#SendJson(req);
|
||||
this.Stats.EventsSent++;
|
||||
this._UpdateState();
|
||||
this.#UpdateState();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to data from this connection
|
||||
*/
|
||||
AddSubscription(sub: Subscriptions) {
|
||||
if (!this.Settings.read) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check relay supports search
|
||||
if (sub.Search && !this.SupportsNip(Nips.Search)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.Subscriptions.has(sub.Id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sub.Started.set(this.Address, new Date().getTime());
|
||||
this._SendSubscription(sub);
|
||||
this.Subscriptions.set(sub.Id, sub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a subscription
|
||||
*/
|
||||
RemoveSubscription(subId: string) {
|
||||
if (this.Subscriptions.has(subId)) {
|
||||
const req = ["CLOSE", subId];
|
||||
this._SendJson(req);
|
||||
this.Subscriptions.delete(subId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook status for connection
|
||||
*/
|
||||
@ -312,80 +283,81 @@ export class Connection {
|
||||
return this.Info?.supported_nips?.some((a) => a === n) ?? false;
|
||||
}
|
||||
|
||||
_UpdateState() {
|
||||
/**
|
||||
* Queue or send command to the relay
|
||||
* @param cmd The REQ to send to the server
|
||||
*/
|
||||
QueueReq(cmd: ReqCommand) {
|
||||
if (this.ActiveRequests.size >= this.#maxSubscriptions) {
|
||||
this.PendingRequests.push(cmd);
|
||||
console.debug("Queuing:", this.Address, cmd);
|
||||
} else {
|
||||
this.ActiveRequests.add(cmd[1]);
|
||||
this.#SendJson(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
CloseReq(id: string) {
|
||||
if (this.ActiveRequests.delete(id)) {
|
||||
this.#SendJson(["CLOSE", id]);
|
||||
this.#SendQueuedRequests();
|
||||
}
|
||||
}
|
||||
|
||||
#SendQueuedRequests() {
|
||||
const canSend = this.#maxSubscriptions - this.ActiveRequests.size;
|
||||
if (canSend > 0) {
|
||||
for (let x = 0; x < canSend; x++) {
|
||||
const cmd = this.PendingRequests.shift();
|
||||
if (cmd) {
|
||||
this.ActiveRequests.add(cmd[1]);
|
||||
this.#SendJson(cmd);
|
||||
console.debug("Sent pending REQ", this.Address, cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ResetQueues() {
|
||||
this.ActiveRequests.clear();
|
||||
this.PendingRequests = [];
|
||||
this.PendingRaw = [];
|
||||
}
|
||||
|
||||
#UpdateState() {
|
||||
this.CurrentState.connected = this.Socket?.readyState === WebSocket.OPEN;
|
||||
this.CurrentState.events.received = this.Stats.EventsReceived;
|
||||
this.CurrentState.events.send = this.Stats.EventsSent;
|
||||
this.CurrentState.avgLatency =
|
||||
this.Stats.Latency.length > 0
|
||||
? this.Stats.Latency.reduce((acc, v) => acc + v, 0) /
|
||||
this.Stats.Latency.length
|
||||
this.Stats.Latency.length
|
||||
: 0;
|
||||
this.CurrentState.disconnects = this.Stats.Disconnects;
|
||||
this.CurrentState.info = this.Info;
|
||||
this.CurrentState.id = this.Id;
|
||||
this.Stats.Latency = this.Stats.Latency.slice(-20); // trim
|
||||
this.HasStateChange = true;
|
||||
this._NotifyState();
|
||||
this.#NotifyState();
|
||||
}
|
||||
|
||||
_NotifyState() {
|
||||
#NotifyState() {
|
||||
const state = this.GetState();
|
||||
for (const [, h] of this.StateHooks) {
|
||||
h(state);
|
||||
}
|
||||
}
|
||||
|
||||
_InitSubscriptions() {
|
||||
// send pending
|
||||
for (const p of this.Pending) {
|
||||
this._SendJson(p);
|
||||
}
|
||||
this.Pending = [];
|
||||
|
||||
for (const [, s] of this.Subscriptions) {
|
||||
this._SendSubscription(s);
|
||||
}
|
||||
this._UpdateState();
|
||||
}
|
||||
|
||||
_SendSubscription(sub: Subscriptions) {
|
||||
if (!this.Authed && this.AwaitingAuth.size > 0) {
|
||||
this.Pending.push(sub.ToObject());
|
||||
return;
|
||||
}
|
||||
|
||||
let req = ["REQ", sub.Id, sub.ToObject()];
|
||||
if (sub.OrSubs.length > 0) {
|
||||
req = [...req, ...sub.OrSubs.map((o) => o.ToObject())];
|
||||
}
|
||||
sub.Started.set(this.Address, new Date().getTime());
|
||||
this._SendJson(req);
|
||||
}
|
||||
|
||||
_SendJson(obj: object) {
|
||||
if (this.Socket?.readyState !== WebSocket.OPEN) {
|
||||
this.Pending.push(obj);
|
||||
#SendJson(obj: object) {
|
||||
const authPending = !this.Authed && this.AwaitingAuth.size > 0;
|
||||
if (this.Socket?.readyState !== WebSocket.OPEN || authPending) {
|
||||
this.PendingRaw.push(obj);
|
||||
return;
|
||||
}
|
||||
const json = JSON.stringify(obj);
|
||||
this.Socket.send(json);
|
||||
}
|
||||
|
||||
_OnEvent(subId: string, ev: RawEvent) {
|
||||
if (this.Subscriptions.has(subId)) {
|
||||
//this._VerifySig(ev);
|
||||
const tagged: TaggedRawEvent = {
|
||||
...ev,
|
||||
relays: [this.Address],
|
||||
};
|
||||
this.Subscriptions.get(subId)?.OnEvent(tagged);
|
||||
} else {
|
||||
// console.warn(`No subscription for event! ${subId}`);
|
||||
// ignored for now, track as "dropped event" with connection stats
|
||||
}
|
||||
}
|
||||
|
||||
async _OnAuthAsync(challenge: string): Promise<void> {
|
||||
const authCleanup = () => {
|
||||
this.AwaitingAuth.delete(challenge);
|
||||
@ -406,61 +378,23 @@ export class Connection {
|
||||
resolve();
|
||||
}, 10_000);
|
||||
|
||||
this.EventsCallback.set(authEvent.Id, (msg: boolean[]) => {
|
||||
this.EventsCallback.set(authEvent.id, (msg: boolean[]) => {
|
||||
clearTimeout(t);
|
||||
authCleanup();
|
||||
if (msg.length > 3 && msg[2] === true) {
|
||||
this.Authed = true;
|
||||
this._InitSubscriptions();
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
const req = ["AUTH", authEvent.ToObject()];
|
||||
this._SendJson(req);
|
||||
const req = ["AUTH", authEvent];
|
||||
this.#SendJson(req);
|
||||
this.Stats.EventsSent++;
|
||||
this._UpdateState();
|
||||
this.#UpdateState();
|
||||
});
|
||||
}
|
||||
|
||||
_OnEnd(subId: string) {
|
||||
const sub = this.Subscriptions.get(subId);
|
||||
if (sub) {
|
||||
const now = new Date().getTime();
|
||||
const started = sub.Started.get(this.Address);
|
||||
sub.Finished.set(this.Address, now);
|
||||
if (started) {
|
||||
const responseTime = now - started;
|
||||
if (responseTime > 10_000) {
|
||||
console.warn(
|
||||
`[${this.Address}][${subId}] Slow response time ${(
|
||||
responseTime / 1000
|
||||
).toFixed(1)} seconds`
|
||||
);
|
||||
}
|
||||
this.Stats.Latency.push(responseTime);
|
||||
} else {
|
||||
console.warn("No started timestamp!");
|
||||
}
|
||||
sub.OnEnd(this);
|
||||
this._UpdateState();
|
||||
} else {
|
||||
console.warn(`No subscription for end! ${subId}`);
|
||||
}
|
||||
}
|
||||
|
||||
_VerifySig(ev: RawEvent) {
|
||||
const payload = [0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content];
|
||||
|
||||
const payloadData = new TextEncoder().encode(JSON.stringify(payload));
|
||||
if (secp.utils.sha256Sync === undefined) {
|
||||
throw "Cannot verify event, no sync sha256 method";
|
||||
}
|
||||
const data = secp.utils.sha256Sync(payloadData);
|
||||
const hash = secp.utils.bytesToHex(data);
|
||||
if (!secp.schnorr.verifySync(ev.sig, hash, ev.pubkey)) {
|
||||
throw "Sig verify failed";
|
||||
}
|
||||
return ev;
|
||||
get #maxSubscriptions() {
|
||||
return this.Info?.limitation?.max_subscriptions ?? 25;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
/**
|
||||
* Websocket re-connect timeout
|
||||
*/
|
||||
export const DefaultConnectTimeout = 2000;
|
||||
export const DefaultConnectTimeout = 2000;
|
||||
|
@ -1,213 +0,0 @@
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import { sha256 } from "@noble/hashes/sha256";
|
||||
import * as base64 from "@protobufjs/base64";
|
||||
import { HexKey, RawEvent, TaggedRawEvent } from "./index";
|
||||
import EventKind from "./EventKind";
|
||||
import Tag from "./Tag";
|
||||
import Thread from "./Thread";
|
||||
|
||||
export default class Event {
|
||||
/**
|
||||
* The original event
|
||||
*/
|
||||
Original: TaggedRawEvent | null;
|
||||
|
||||
/**
|
||||
* Id of the event
|
||||
*/
|
||||
Id: string;
|
||||
|
||||
/**
|
||||
* Pub key of the creator
|
||||
*/
|
||||
PubKey: string;
|
||||
|
||||
/**
|
||||
* Timestamp when the event was created
|
||||
*/
|
||||
CreatedAt: number;
|
||||
|
||||
/**
|
||||
* The type of event
|
||||
*/
|
||||
Kind: EventKind;
|
||||
|
||||
/**
|
||||
* A list of metadata tags
|
||||
*/
|
||||
Tags: Array<Tag>;
|
||||
|
||||
/**
|
||||
* Content of the event
|
||||
*/
|
||||
Content: string;
|
||||
|
||||
/**
|
||||
* Signature of this event from the creator
|
||||
*/
|
||||
Signature: string;
|
||||
|
||||
/**
|
||||
* Thread information for this event
|
||||
*/
|
||||
Thread: Thread | null;
|
||||
|
||||
constructor(e?: TaggedRawEvent) {
|
||||
this.Original = e ?? null;
|
||||
this.Id = e?.id ?? "";
|
||||
this.PubKey = e?.pubkey ?? "";
|
||||
this.CreatedAt = e?.created_at ?? Math.floor(new Date().getTime() / 1000);
|
||||
this.Kind = e?.kind ?? EventKind.Unknown;
|
||||
this.Tags = e?.tags.map((a, i) => new Tag(a, i)) ?? [];
|
||||
this.Content = e?.content ?? "";
|
||||
this.Signature = e?.sig ?? "";
|
||||
this.Thread = Thread.ExtractThread(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pub key of the creator of this event NIP-26
|
||||
*/
|
||||
get RootPubKey() {
|
||||
const delegation = this.Tags.find((a) => a.Key === "delegation");
|
||||
if (delegation?.PubKey) {
|
||||
return delegation.PubKey;
|
||||
}
|
||||
return this.PubKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign this message with a private key
|
||||
*/
|
||||
async Sign(key: HexKey) {
|
||||
this.Id = this.CreateId();
|
||||
|
||||
const sig = await secp.schnorr.sign(this.Id, key);
|
||||
this.Signature = secp.utils.bytesToHex(sig);
|
||||
if (!(await this.Verify())) {
|
||||
throw "Signing failed";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the signature of this message
|
||||
* @returns True if valid signature
|
||||
*/
|
||||
async Verify() {
|
||||
const id = this.CreateId();
|
||||
const result = await secp.schnorr.verify(this.Signature, id, this.PubKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
CreateId() {
|
||||
const payload = [
|
||||
0,
|
||||
this.PubKey,
|
||||
this.CreatedAt,
|
||||
this.Kind,
|
||||
this.Tags.map((a) => a.ToObject()).filter((a) => a !== null),
|
||||
this.Content,
|
||||
];
|
||||
|
||||
const hash = secp.utils.bytesToHex(sha256(JSON.stringify(payload)));
|
||||
if (this.Id !== "" && hash !== this.Id) {
|
||||
console.debug(payload);
|
||||
throw "ID doesnt match!";
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
ToObject(): RawEvent {
|
||||
return {
|
||||
id: this.Id,
|
||||
pubkey: this.PubKey,
|
||||
created_at: this.CreatedAt,
|
||||
kind: this.Kind,
|
||||
tags: <string[][]>this.Tags.sort((a, b) => a.Index - b.Index)
|
||||
.map((a) => a.ToObject())
|
||||
.filter((a) => a !== null),
|
||||
content: this.Content,
|
||||
sig: this.Signature,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new event for a specific pubkey
|
||||
*/
|
||||
static ForPubKey(pubKey: HexKey) {
|
||||
const ev = new Event();
|
||||
ev.PubKey = pubKey;
|
||||
return ev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the given message content
|
||||
*/
|
||||
async EncryptData(content: string, pubkey: HexKey, privkey: HexKey) {
|
||||
const key = await this._GetDmSharedKey(pubkey, privkey);
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
const data = new TextEncoder().encode(content);
|
||||
const result = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
data
|
||||
);
|
||||
const uData = new Uint8Array(result);
|
||||
return `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode(
|
||||
iv,
|
||||
0,
|
||||
16
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the message content in place
|
||||
*/
|
||||
async EncryptDmForPubkey(pubkey: HexKey, privkey: HexKey) {
|
||||
this.Content = await this.EncryptData(this.Content, pubkey, privkey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the content of the message
|
||||
*/
|
||||
async DecryptData(cyphertext: string, privkey: HexKey, pubkey: HexKey) {
|
||||
const key = await this._GetDmSharedKey(pubkey, privkey);
|
||||
const cSplit = cyphertext.split("?iv=");
|
||||
const data = new Uint8Array(base64.length(cSplit[0]));
|
||||
base64.decode(cSplit[0], data, 0);
|
||||
|
||||
const iv = new Uint8Array(base64.length(cSplit[1]));
|
||||
base64.decode(cSplit[1], iv, 0);
|
||||
|
||||
const result = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
data
|
||||
);
|
||||
return new TextDecoder().decode(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the content of this message in place
|
||||
*/
|
||||
async DecryptDm(privkey: HexKey, pubkey: HexKey) {
|
||||
this.Content = await this.DecryptData(this.Content, privkey, pubkey);
|
||||
}
|
||||
|
||||
async _GetDmSharedKey(pubkey: HexKey, privkey: HexKey) {
|
||||
const sharedPoint = secp.getSharedSecret(privkey, "02" + pubkey);
|
||||
const sharedX = sharedPoint.slice(1, 33);
|
||||
return await window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
sharedX,
|
||||
{ name: "AES-CBC" },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
enum Nips {
|
||||
export enum Nips {
|
||||
Search = 50,
|
||||
}
|
||||
|
||||
export default Nips;
|
||||
|
@ -8,5 +8,8 @@ export interface RelayInfo {
|
||||
version?: string;
|
||||
limitation?: {
|
||||
payment_required: boolean;
|
||||
max_subscriptions: number;
|
||||
max_filters: number;
|
||||
max_event_tags: number;
|
||||
};
|
||||
}
|
||||
|
@ -1,176 +0,0 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { TaggedRawEvent, RawReqFilter, u256 } from "./index";
|
||||
import { Connection } from "./Connection";
|
||||
import EventKind from "./EventKind";
|
||||
|
||||
export type NEventHandler = (e: TaggedRawEvent) => void;
|
||||
export type OnEndHandler = (c: Connection) => void;
|
||||
|
||||
export class Subscriptions {
|
||||
/**
|
||||
* A unique id for this subscription filter
|
||||
*/
|
||||
Id: u256;
|
||||
|
||||
/**
|
||||
* a list of event ids or prefixes
|
||||
*/
|
||||
Ids?: Set<u256>;
|
||||
|
||||
/**
|
||||
* a list of pubkeys or prefixes, the pubkey of an event must be one of these
|
||||
*/
|
||||
Authors?: Set<u256>;
|
||||
|
||||
/**
|
||||
* a list of a kind numbers
|
||||
*/
|
||||
Kinds?: Set<EventKind>;
|
||||
|
||||
/**
|
||||
* a list of event ids that are referenced in an "e" tag
|
||||
*/
|
||||
ETags?: Set<u256>;
|
||||
|
||||
/**
|
||||
* a list of pubkeys that are referenced in a "p" tag
|
||||
*/
|
||||
PTags?: Set<u256>;
|
||||
|
||||
/**
|
||||
* A list of "t" tags to search
|
||||
*/
|
||||
HashTags?: Set<string>;
|
||||
|
||||
/**
|
||||
* A litst of "d" tags to search
|
||||
*/
|
||||
DTags?: Set<string>;
|
||||
|
||||
/**
|
||||
* A litst of "r" tags to search
|
||||
*/
|
||||
RTags?: Set<string>;
|
||||
|
||||
/**
|
||||
* A list of search terms
|
||||
*/
|
||||
Search?: string;
|
||||
|
||||
/**
|
||||
* a timestamp, events must be newer than this to pass
|
||||
*/
|
||||
Since?: number;
|
||||
|
||||
/**
|
||||
* a timestamp, events must be older than this to pass
|
||||
*/
|
||||
Until?: number;
|
||||
|
||||
/**
|
||||
* maximum number of events to be returned in the initial query
|
||||
*/
|
||||
Limit?: number;
|
||||
|
||||
/**
|
||||
* Handler function for this event
|
||||
*/
|
||||
OnEvent: NEventHandler;
|
||||
|
||||
/**
|
||||
* End of data event
|
||||
*/
|
||||
OnEnd: OnEndHandler;
|
||||
|
||||
/**
|
||||
* Collection of OR sub scriptions linked to this
|
||||
*/
|
||||
OrSubs: Array<Subscriptions>;
|
||||
|
||||
/**
|
||||
* Start time for this subscription
|
||||
*/
|
||||
Started: Map<string, number>;
|
||||
|
||||
/**
|
||||
* End time for this subscription
|
||||
*/
|
||||
Finished: Map<string, number>;
|
||||
|
||||
constructor(sub?: RawReqFilter) {
|
||||
this.Id = uuid();
|
||||
this.Ids = sub?.ids ? new Set(sub.ids) : undefined;
|
||||
this.Authors = sub?.authors ? new Set(sub.authors) : undefined;
|
||||
this.Kinds = sub?.kinds ? new Set(sub.kinds) : undefined;
|
||||
this.ETags = sub?.["#e"] ? new Set(sub["#e"]) : undefined;
|
||||
this.PTags = sub?.["#p"] ? new Set(sub["#p"]) : undefined;
|
||||
this.DTags = sub?.["#d"] ? new Set(["#d"]) : undefined;
|
||||
this.RTags = sub?.["#r"] ? new Set(["#r"]) : undefined;
|
||||
this.Search = sub?.search ?? undefined;
|
||||
this.Since = sub?.since ?? undefined;
|
||||
this.Until = sub?.until ?? undefined;
|
||||
this.Limit = sub?.limit ?? undefined;
|
||||
this.OnEvent = () => {
|
||||
console.warn(`No event handler was set on subscription: ${this.Id}`);
|
||||
};
|
||||
this.OnEnd = () => undefined;
|
||||
this.OrSubs = [];
|
||||
this.Started = new Map<string, number>();
|
||||
this.Finished = new Map<string, number>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds OR filter subscriptions
|
||||
*/
|
||||
AddSubscription(sub: Subscriptions) {
|
||||
this.OrSubs.push(sub);
|
||||
}
|
||||
|
||||
/**
|
||||
* If all relays have responded with EOSE
|
||||
*/
|
||||
IsFinished() {
|
||||
return this.Started.size === this.Finished.size;
|
||||
}
|
||||
|
||||
ToObject(): RawReqFilter {
|
||||
const ret: RawReqFilter = {};
|
||||
if (this.Ids) {
|
||||
ret.ids = Array.from(this.Ids);
|
||||
}
|
||||
if (this.Authors) {
|
||||
ret.authors = Array.from(this.Authors);
|
||||
}
|
||||
if (this.Kinds) {
|
||||
ret.kinds = Array.from(this.Kinds);
|
||||
}
|
||||
if (this.ETags) {
|
||||
ret["#e"] = Array.from(this.ETags);
|
||||
}
|
||||
if (this.PTags) {
|
||||
ret["#p"] = Array.from(this.PTags);
|
||||
}
|
||||
if (this.HashTags) {
|
||||
ret["#t"] = Array.from(this.HashTags);
|
||||
}
|
||||
if (this.DTags) {
|
||||
ret["#d"] = Array.from(this.DTags);
|
||||
}
|
||||
if (this.RTags) {
|
||||
ret["#r"] = Array.from(this.RTags);
|
||||
}
|
||||
if (this.Search) {
|
||||
ret.search = this.Search;
|
||||
}
|
||||
if (this.Since !== null) {
|
||||
ret.since = this.Since;
|
||||
}
|
||||
if (this.Until !== null) {
|
||||
ret.until = this.Until;
|
||||
}
|
||||
if (this.Limit !== null) {
|
||||
ret.limit = this.Limit;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import { u256 } from "./index";
|
||||
import { default as NEvent } from "./Event";
|
||||
import EventKind from "./EventKind";
|
||||
import Tag from "./Tag";
|
||||
|
||||
export default class Thread {
|
||||
Root?: Tag;
|
||||
ReplyTo?: Tag;
|
||||
Mentions: Array<Tag>;
|
||||
PubKeys: Array<u256>;
|
||||
|
||||
constructor() {
|
||||
this.Mentions = [];
|
||||
this.PubKeys = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract thread information from an Event
|
||||
* @param ev Event to extract thread from
|
||||
*/
|
||||
static ExtractThread(ev: NEvent) {
|
||||
const isThread = ev.Tags.some((a) => a.Key === "e" && a.Marker !== "mention");
|
||||
if (!isThread) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const shouldWriteMarkers = ev.Kind === EventKind.TextNote;
|
||||
const ret = new Thread();
|
||||
const eTags = ev.Tags.filter((a) => a.Key === "e");
|
||||
const marked = eTags.some((a) => a.Marker !== undefined);
|
||||
if (!marked) {
|
||||
ret.Root = eTags[0];
|
||||
ret.Root.Marker = shouldWriteMarkers ? "root" : undefined;
|
||||
if (eTags.length > 1) {
|
||||
ret.ReplyTo = eTags[1];
|
||||
ret.ReplyTo.Marker = shouldWriteMarkers ? "reply" : undefined;
|
||||
}
|
||||
if (eTags.length > 2) {
|
||||
ret.Mentions = eTags.slice(2);
|
||||
if (shouldWriteMarkers) {
|
||||
ret.Mentions.forEach((a) => (a.Marker = "mention"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const root = eTags.find((a) => a.Marker === "root");
|
||||
const reply = eTags.find((a) => a.Marker === "reply");
|
||||
ret.Root = root;
|
||||
ret.ReplyTo = reply;
|
||||
ret.Mentions = eTags.filter((a) => a.Marker === "mention");
|
||||
}
|
||||
ret.PubKeys = Array.from(
|
||||
new Set(ev.Tags.filter((a) => a.Key === "p").map((a) => <u256>a.PubKey))
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
}
|
@ -24,3 +24,11 @@ export function hexToBech32(hrp: string, hex?: string) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeRelayUrl(url: string) {
|
||||
try {
|
||||
return new URL(url).toString();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
export * from "./Connection";
|
||||
export { default as EventKind } from "./EventKind";
|
||||
export { Subscriptions } from "./Subscriptions";
|
||||
export { default as Event } from "./Event";
|
||||
export { default as Tag } from "./Tag";
|
||||
export * from "./Links";
|
||||
export * from "./Nips";
|
||||
|
||||
import { RelaySettings } from ".";
|
||||
export type RawEvent = {
|
||||
id: u256;
|
||||
pubkey: HexKey;
|
||||
created_at: number;
|
||||
kind: number;
|
||||
tags: string[][];
|
||||
tags: Array<Array<string>>;
|
||||
content: string;
|
||||
sig: string;
|
||||
};
|
||||
@ -37,6 +37,8 @@ export type MaybeHexKey = HexKey | undefined;
|
||||
*/
|
||||
export type u256 = string;
|
||||
|
||||
export type ReqCommand = [cmd: "REQ", id: string, ...filters: Array<RawReqFilter>];
|
||||
|
||||
/**
|
||||
* Raw REQ filter object
|
||||
*/
|
||||
@ -83,5 +85,5 @@ export enum Lists {
|
||||
|
||||
export interface FullRelaySettings {
|
||||
url: string;
|
||||
settings: { read: boolean; write: boolean };
|
||||
settings: RelaySettings;
|
||||
}
|
||||
|
Reference in New Issue
Block a user