4409 lines
130 KiB
JavaScript
4409 lines
130 KiB
JavaScript
"use strict";
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// src/index.ts
|
|
var src_exports = {};
|
|
__export(src_exports, {
|
|
NDKAppHandlerEvent: () => NDKAppHandlerEvent,
|
|
NDKArticle: () => NDKArticle,
|
|
NDKDVMJobFeedback: () => NDKDVMJobFeedback,
|
|
NDKDVMJobResult: () => NDKDVMJobResult,
|
|
NDKDVMRequest: () => NDKDVMRequest,
|
|
NDKDvmJobFeedbackStatus: () => NDKDvmJobFeedbackStatus,
|
|
NDKEvent: () => NDKEvent,
|
|
NDKHighlight: () => NDKHighlight,
|
|
NDKKind: () => NDKKind,
|
|
NDKList: () => NDKList,
|
|
NDKListKinds: () => NDKListKinds,
|
|
NDKNip07Signer: () => NDKNip07Signer,
|
|
NDKNip46Backend: () => NDKNip46Backend,
|
|
NDKNip46Signer: () => NDKNip46Signer,
|
|
NDKNostrRpc: () => NDKNostrRpc,
|
|
NDKPrivateKeySigner: () => NDKPrivateKeySigner,
|
|
NDKRelay: () => NDKRelay,
|
|
NDKRelayList: () => NDKRelayList,
|
|
NDKRelaySet: () => NDKRelaySet,
|
|
NDKRelayStatus: () => NDKRelayStatus,
|
|
NDKRepost: () => NDKRepost,
|
|
NDKSubscription: () => NDKSubscription,
|
|
NDKSubscriptionCacheUsage: () => NDKSubscriptionCacheUsage,
|
|
NDKTranscriptionDVM: () => NDKTranscriptionDVM,
|
|
NDKUser: () => NDKUser,
|
|
calculateGroupableId: () => calculateGroupableId,
|
|
default: () => NDK,
|
|
defaultOpts: () => defaultOpts,
|
|
mergeFilters: () => mergeFilters,
|
|
profileFromEvent: () => profileFromEvent,
|
|
serializeProfile: () => serializeProfile,
|
|
zapInvoiceFromEvent: () => zapInvoiceFromEvent
|
|
});
|
|
module.exports = __toCommonJS(src_exports);
|
|
|
|
// src/user/index.ts
|
|
var import_nostr_tools8 = require("nostr-tools");
|
|
|
|
// src/events/index.ts
|
|
var import_eventemitter34 = __toESM(require("eventemitter3"));
|
|
var import_nostr_tools7 = require("nostr-tools");
|
|
|
|
// src/relay/index.ts
|
|
var import_debug = __toESM(require("debug"));
|
|
var import_eventemitter32 = __toESM(require("eventemitter3"));
|
|
|
|
// src/relay/connectivity.ts
|
|
var import_nostr_tools = require("nostr-tools");
|
|
var NDKRelayConnectivity = class {
|
|
ndkRelay;
|
|
_status;
|
|
relay;
|
|
connectedAt;
|
|
_connectionStats = {
|
|
attempts: 0,
|
|
success: 0,
|
|
durations: []
|
|
};
|
|
debug;
|
|
constructor(ndkRelay) {
|
|
this.ndkRelay = ndkRelay;
|
|
this._status = 3 /* DISCONNECTED */;
|
|
this.relay = (0, import_nostr_tools.relayInit)(this.ndkRelay.url);
|
|
this.debug = this.ndkRelay.debug.extend("connectivity");
|
|
this.relay.on("notice", (notice) => this.handleNotice(notice));
|
|
}
|
|
async connect() {
|
|
const connectHandler = () => {
|
|
this.updateConnectionStats.connected();
|
|
this._status = 1 /* CONNECTED */;
|
|
this.ndkRelay.emit("connect");
|
|
};
|
|
const disconnectHandler = () => {
|
|
this.updateConnectionStats.disconnected();
|
|
if (this._status === 1 /* CONNECTED */) {
|
|
this._status = 3 /* DISCONNECTED */;
|
|
this.handleReconnection();
|
|
}
|
|
this.ndkRelay.emit("disconnect");
|
|
};
|
|
try {
|
|
this.updateConnectionStats.attempt();
|
|
this._status = 0 /* CONNECTING */;
|
|
this.relay.off("connect", connectHandler);
|
|
this.relay.off("disconnect", disconnectHandler);
|
|
this.relay.on("connect", connectHandler);
|
|
this.relay.on("disconnect", disconnectHandler);
|
|
await this.relay.connect();
|
|
} catch (e) {
|
|
this.debug("Failed to connect", e);
|
|
this._status = 3 /* DISCONNECTED */;
|
|
throw e;
|
|
}
|
|
}
|
|
disconnect() {
|
|
this._status = 2 /* DISCONNECTING */;
|
|
this.relay.close();
|
|
}
|
|
get status() {
|
|
return this._status;
|
|
}
|
|
isAvailable() {
|
|
return this._status === 1 /* CONNECTED */;
|
|
}
|
|
/**
|
|
* Evaluates the connection stats to determine if the relay is flapping.
|
|
*/
|
|
isFlapping() {
|
|
const durations = this._connectionStats.durations;
|
|
if (durations.length % 3 !== 0)
|
|
return false;
|
|
const sum = durations.reduce((a, b) => a + b, 0);
|
|
const avg = sum / durations.length;
|
|
const variance = durations.map((x) => Math.pow(x - avg, 2)).reduce((a, b) => a + b, 0) / durations.length;
|
|
const stdDev = Math.sqrt(variance);
|
|
const isFlapping = stdDev < 1e3;
|
|
return isFlapping;
|
|
}
|
|
async handleNotice(notice) {
|
|
if (notice.includes("oo many") || notice.includes("aximum")) {
|
|
this.disconnect();
|
|
setTimeout(() => this.connect(), 2e3);
|
|
this.debug(this.relay.url, "Relay complaining?", notice);
|
|
}
|
|
this.ndkRelay.emit("notice", this, notice);
|
|
}
|
|
/**
|
|
* Called when the relay is unexpectedly disconnected.
|
|
*/
|
|
handleReconnection(attempt = 0) {
|
|
if (this.isFlapping()) {
|
|
this.ndkRelay.emit("flapping", this, this._connectionStats);
|
|
this._status = 5 /* FLAPPING */;
|
|
return;
|
|
}
|
|
const reconnectDelay = this.connectedAt ? Math.max(0, 6e4 - (Date.now() - this.connectedAt)) : 0;
|
|
setTimeout(() => {
|
|
this._status = 4 /* RECONNECTING */;
|
|
this.connect().then(() => {
|
|
this.debug("Reconnected");
|
|
}).catch((err) => {
|
|
this.debug("Reconnect failed", err);
|
|
if (attempt < 5) {
|
|
setTimeout(() => {
|
|
this.handleReconnection(attempt + 1);
|
|
}, 6e4);
|
|
} else {
|
|
this.debug("Reconnect failed after 5 attempts");
|
|
}
|
|
});
|
|
}, reconnectDelay);
|
|
}
|
|
/**
|
|
* Utility functions to update the connection stats.
|
|
*/
|
|
updateConnectionStats = {
|
|
connected: () => {
|
|
this._connectionStats.success++;
|
|
this._connectionStats.connectedAt = Date.now();
|
|
},
|
|
disconnected: () => {
|
|
if (this._connectionStats.connectedAt) {
|
|
this._connectionStats.durations.push(
|
|
Date.now() - this._connectionStats.connectedAt
|
|
);
|
|
if (this._connectionStats.durations.length > 100) {
|
|
this._connectionStats.durations.shift();
|
|
}
|
|
}
|
|
this._connectionStats.connectedAt = void 0;
|
|
},
|
|
attempt: () => {
|
|
this._connectionStats.attempts++;
|
|
}
|
|
};
|
|
/**
|
|
* Returns the connection stats.
|
|
*/
|
|
get connectionStats() {
|
|
return this._connectionStats;
|
|
}
|
|
};
|
|
|
|
// src/relay/publisher.ts
|
|
var NDKRelayPublisher = class {
|
|
ndkRelay;
|
|
constructor(ndkRelay) {
|
|
this.ndkRelay = ndkRelay;
|
|
}
|
|
/**
|
|
* Published an event to the relay; if the relay is not connected, it will
|
|
* wait for the relay to connect before publishing the event.
|
|
*
|
|
* If the relay does not connect within the timeout, the publish operation
|
|
* will fail.
|
|
* @param event The event to publish
|
|
* @param timeoutMs The timeout for the publish operation in milliseconds
|
|
* @returns A promise that resolves when the event has been published or rejects if the operation times out
|
|
*/
|
|
async publish(event, timeoutMs = 2500) {
|
|
const publishWhenConnected = () => {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
this.publishEvent(event, timeoutMs).then((result) => resolve(result)).catch((err) => reject(err));
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
};
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
setTimeout(() => reject(new Error("Timeout")), timeoutMs);
|
|
});
|
|
const onConnectHandler = () => {
|
|
publishWhenConnected().then((result) => connectResolve(result)).catch((err) => connectReject(err));
|
|
};
|
|
let connectResolve;
|
|
let connectReject;
|
|
if (this.ndkRelay.status === 1 /* CONNECTED */) {
|
|
return Promise.race([publishWhenConnected(), timeoutPromise]);
|
|
} else {
|
|
return Promise.race([
|
|
new Promise((resolve, reject) => {
|
|
connectResolve = resolve;
|
|
connectReject = reject;
|
|
this.ndkRelay.once("connect", onConnectHandler);
|
|
}),
|
|
timeoutPromise
|
|
]).finally(() => {
|
|
this.ndkRelay.removeListener("connect", onConnectHandler);
|
|
});
|
|
}
|
|
}
|
|
async publishEvent(event, timeoutMs) {
|
|
const nostrEvent = await event.toNostrEvent();
|
|
const publish = this.ndkRelay.connectivity.relay.publish(nostrEvent);
|
|
let publishTimeout;
|
|
const publishPromise = new Promise((resolve, reject) => {
|
|
publish.then(() => {
|
|
clearTimeout(publishTimeout);
|
|
this.ndkRelay.emit("published", event);
|
|
resolve(true);
|
|
}).catch((err) => {
|
|
clearTimeout(publishTimeout);
|
|
this.ndkRelay.debug("Publish failed", err, event.id);
|
|
this.ndkRelay.emit("publish:failed", event, err);
|
|
reject(err);
|
|
});
|
|
});
|
|
if (!timeoutMs || event.isEphemeral()) {
|
|
return publishPromise;
|
|
}
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
publishTimeout = setTimeout(() => {
|
|
this.ndkRelay.debug("Publish timed out", event.rawEvent());
|
|
this.ndkRelay.emit("publish:failed", event, "Timeout");
|
|
reject(new Error("Publish operation timed out"));
|
|
}, timeoutMs);
|
|
});
|
|
return Promise.race([publishPromise, timeoutPromise]);
|
|
}
|
|
};
|
|
|
|
// src/relay/subscriptions.ts
|
|
var import_eventemitter3 = __toESM(require("eventemitter3"));
|
|
var import_nostr_tools3 = require("nostr-tools");
|
|
|
|
// src/subscription/grouping.ts
|
|
function calculateGroupableId(filters) {
|
|
const elements = [];
|
|
for (const filter of filters) {
|
|
const hasTimeConstraints = filter.since || filter.until;
|
|
if (hasTimeConstraints)
|
|
return null;
|
|
const keys = Object.keys(filter || {}).sort().join("-");
|
|
elements.push(keys);
|
|
}
|
|
return elements.join("|");
|
|
}
|
|
function mergeFilters(filters) {
|
|
const result = {};
|
|
filters.forEach((filter) => {
|
|
Object.entries(filter).forEach(([key, value]) => {
|
|
if (Array.isArray(value)) {
|
|
if (result[key] === void 0) {
|
|
result[key] = [...value];
|
|
} else {
|
|
result[key] = Array.from(/* @__PURE__ */ new Set([...result[key], ...value]));
|
|
}
|
|
} else {
|
|
result[key] = value;
|
|
}
|
|
});
|
|
});
|
|
return result;
|
|
}
|
|
|
|
// src/subscription/utils.ts
|
|
var import_nostr_tools2 = require("nostr-tools");
|
|
function queryFullyFilled(subscription) {
|
|
if (filterIncludesIds(subscription.filter)) {
|
|
if (resultHasAllRequestedIds(subscription)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function compareFilter(filter1, filter2) {
|
|
if (Object.keys(filter1).length !== Object.keys(filter2).length)
|
|
return false;
|
|
for (const [key, value] of Object.entries(filter1)) {
|
|
const valuesInFilter2 = filter2[key];
|
|
if (!valuesInFilter2)
|
|
return false;
|
|
if (Array.isArray(value) && Array.isArray(valuesInFilter2)) {
|
|
const v = value;
|
|
for (const valueInFilter2 of valuesInFilter2) {
|
|
const val = valueInFilter2;
|
|
if (!v.includes(val)) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
if (valuesInFilter2 !== value)
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
function filterIncludesIds(filter) {
|
|
return !!filter["ids"];
|
|
}
|
|
function resultHasAllRequestedIds(subscription) {
|
|
const ids = subscription.filter["ids"];
|
|
return !!ids && ids.length === subscription.eventFirstSeen.size;
|
|
}
|
|
function generateSubId(subscriptions, filters) {
|
|
const subIds = subscriptions.map((sub) => sub.subId).filter(Boolean);
|
|
const subIdParts = [];
|
|
const filterNonKindKeys = /* @__PURE__ */ new Set();
|
|
const filterKinds = /* @__PURE__ */ new Set();
|
|
if (subIds.length > 0) {
|
|
subIdParts.push(Array.from(new Set(subIds)).join(","));
|
|
} else {
|
|
for (const filter of filters) {
|
|
for (const key of Object.keys(filter)) {
|
|
if (key === "kinds") {
|
|
filter.kinds?.forEach((k) => filterKinds.add(k));
|
|
} else {
|
|
filterNonKindKeys.add(key);
|
|
}
|
|
}
|
|
}
|
|
if (filterKinds.size > 0) {
|
|
subIdParts.push("kinds:" + Array.from(filterKinds).join(","));
|
|
}
|
|
if (filterNonKindKeys.size > 0) {
|
|
subIdParts.push(Array.from(filterNonKindKeys).join(","));
|
|
}
|
|
}
|
|
if (subIds.length !== 1) {
|
|
subIdParts.push(Math.floor(Math.random() * 999).toString());
|
|
}
|
|
return subIdParts.join("-");
|
|
}
|
|
function filterFromId(id) {
|
|
let decoded;
|
|
if (id.match(NIP33_A_REGEX)) {
|
|
const [kind, pubkey, identifier] = id.split(":");
|
|
const filter = {
|
|
authors: [pubkey],
|
|
kinds: [parseInt(kind)]
|
|
};
|
|
if (identifier) {
|
|
filter["#d"] = [identifier];
|
|
}
|
|
return filter;
|
|
}
|
|
try {
|
|
decoded = import_nostr_tools2.nip19.decode(id);
|
|
switch (decoded.type) {
|
|
case "nevent":
|
|
return { ids: [decoded.data.id] };
|
|
case "note":
|
|
return { ids: [decoded.data] };
|
|
case "naddr":
|
|
return {
|
|
authors: [decoded.data.pubkey],
|
|
"#d": [decoded.data.identifier],
|
|
kinds: [decoded.data.kind]
|
|
};
|
|
}
|
|
} catch (e) {
|
|
}
|
|
return { ids: [id] };
|
|
}
|
|
function isNip33AValue(value) {
|
|
return value.match(NIP33_A_REGEX) !== null;
|
|
}
|
|
var NIP33_A_REGEX = /^(\d+):([0-9A-Fa-f]+)(?::(.*))?$/;
|
|
function relaysFromBech32(bech322) {
|
|
try {
|
|
const decoded = import_nostr_tools2.nip19.decode(bech322);
|
|
if (["naddr", "nevent"].includes(decoded?.type)) {
|
|
const data = decoded.data;
|
|
if (data?.relays) {
|
|
return data.relays.map((r) => new NDKRelay(r));
|
|
}
|
|
}
|
|
} catch (e) {
|
|
}
|
|
return [];
|
|
}
|
|
|
|
// src/relay/subscriptions.ts
|
|
var NDKGroupedSubscriptions = class extends import_eventemitter3.default {
|
|
subscriptions;
|
|
req;
|
|
debug;
|
|
constructor(subscriptions, debug4) {
|
|
super();
|
|
this.subscriptions = subscriptions;
|
|
this.debug = debug4 || this.subscriptions[0].subscription.debug.extend("grouped");
|
|
for (const subscription of subscriptions) {
|
|
this.handleSubscriptionClosure(subscription);
|
|
}
|
|
}
|
|
/**
|
|
* Adds a subscription to this group.
|
|
* @param subscription
|
|
*/
|
|
addSubscription(subscription) {
|
|
this.subscriptions.push(subscription);
|
|
this.handleSubscriptionClosure(subscription);
|
|
}
|
|
eventReceived(event) {
|
|
for (const subscription of this.subscriptions) {
|
|
subscription.eventReceived(event);
|
|
}
|
|
}
|
|
eoseReceived(relay) {
|
|
const subscriptionsToInform = Array.from(this.subscriptions);
|
|
subscriptionsToInform.forEach(async (subscription) => {
|
|
subscription.subscription.eoseReceived(relay);
|
|
});
|
|
}
|
|
handleSubscriptionClosure(subscription) {
|
|
subscription.subscription.on("close", () => {
|
|
const index = this.subscriptions.findIndex(
|
|
(i) => i.subscription === subscription.subscription
|
|
);
|
|
this.subscriptions.splice(index, 1);
|
|
if (this.subscriptions.length <= 0) {
|
|
this.emit("close");
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Maps each subscription through a transformation function.
|
|
* @param fn - The transformation function.
|
|
* @returns A new array with each subscription transformed by fn.
|
|
*/
|
|
map(fn) {
|
|
return this.subscriptions.map(fn);
|
|
}
|
|
[Symbol.iterator]() {
|
|
let index = 0;
|
|
const subscriptions = this.subscriptions;
|
|
return {
|
|
next() {
|
|
if (index < subscriptions.length) {
|
|
return { value: subscriptions[index++], done: false };
|
|
} else {
|
|
return { value: null, done: true };
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|
|
var NDKSubscriptionFilters = class {
|
|
subscription;
|
|
filters = [];
|
|
ndkRelay;
|
|
constructor(subscription, filters, ndkRelay) {
|
|
this.subscription = subscription;
|
|
this.filters = filters;
|
|
this.ndkRelay = ndkRelay;
|
|
}
|
|
eventReceived(event) {
|
|
if (!this.eventMatchesLocalFilter(event))
|
|
return;
|
|
this.subscription.eventReceived(event, this.ndkRelay, false);
|
|
}
|
|
eventMatchesLocalFilter(event) {
|
|
const rawEvent = event.rawEvent();
|
|
return this.filters.some((filter) => (0, import_nostr_tools3.matchFilter)(filter, rawEvent));
|
|
}
|
|
};
|
|
function findMatchingActiveSubscriptions(activeSubscriptions, filters) {
|
|
if (activeSubscriptions.length !== filters.length)
|
|
return false;
|
|
for (let i = 0; i < activeSubscriptions.length; i++) {
|
|
if (!compareFilter(activeSubscriptions[i], filters[i])) {
|
|
break;
|
|
}
|
|
return activeSubscriptions[i];
|
|
}
|
|
return void 0;
|
|
}
|
|
var NDKRelaySubscriptions = class {
|
|
ndkRelay;
|
|
delayedItems = /* @__PURE__ */ new Map();
|
|
delayedTimers = /* @__PURE__ */ new Map();
|
|
/**
|
|
* Active subscriptions this relay is connected to
|
|
*/
|
|
activeSubscriptions = /* @__PURE__ */ new Map();
|
|
activeSubscriptionsByGroupId = /* @__PURE__ */ new Map();
|
|
debug;
|
|
groupingDebug;
|
|
conn;
|
|
constructor(ndkRelay) {
|
|
this.ndkRelay = ndkRelay;
|
|
this.conn = ndkRelay.connectivity;
|
|
this.debug = ndkRelay.debug.extend("subscriptions");
|
|
this.groupingDebug = ndkRelay.debug.extend("grouping");
|
|
}
|
|
/**
|
|
* Creates or queues a subscription to the relay.
|
|
*/
|
|
subscribe(subscription, filters) {
|
|
const groupableId = calculateGroupableId(filters);
|
|
const subscriptionFilters = new NDKSubscriptionFilters(
|
|
subscription,
|
|
filters,
|
|
this.ndkRelay
|
|
);
|
|
if (!groupableId || !subscription.isGroupable()) {
|
|
this.groupingDebug("No groupable ID for filters", filters);
|
|
this.executeSubscriptions(
|
|
groupableId,
|
|
// hacky
|
|
new NDKGroupedSubscriptions([subscriptionFilters]),
|
|
filters
|
|
);
|
|
return;
|
|
}
|
|
const activeSubscriptions = this.activeSubscriptionsByGroupId.get(groupableId);
|
|
if (activeSubscriptions) {
|
|
const matchingSubscription = findMatchingActiveSubscriptions(
|
|
activeSubscriptions.filters,
|
|
filters
|
|
);
|
|
if (matchingSubscription) {
|
|
const activeSubscription = this.activeSubscriptions.get(activeSubscriptions.sub);
|
|
activeSubscription?.addSubscription(
|
|
new NDKSubscriptionFilters(subscription, filters, this.ndkRelay)
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
let delayedItem = this.delayedItems.get(groupableId);
|
|
if (!delayedItem) {
|
|
delayedItem = new NDKGroupedSubscriptions([subscriptionFilters]);
|
|
this.delayedItems.set(groupableId, delayedItem);
|
|
delayedItem.once("close", () => {
|
|
const delayedItem2 = this.delayedItems.get(groupableId);
|
|
if (!delayedItem2)
|
|
return;
|
|
this.delayedItems.delete(groupableId);
|
|
});
|
|
} else {
|
|
delayedItem.addSubscription(subscriptionFilters);
|
|
}
|
|
const timeout = setTimeout(() => {
|
|
this.executeGroup(groupableId, subscription);
|
|
}, subscription.opts.groupableDelay);
|
|
if (this.delayedTimers.has(groupableId)) {
|
|
this.delayedTimers.get(groupableId).push(timeout);
|
|
} else {
|
|
this.delayedTimers.set(groupableId, [timeout]);
|
|
}
|
|
}
|
|
/**
|
|
* Executes a delayed subscription via its groupable ID.
|
|
* @param groupableId
|
|
*/
|
|
executeGroup(groupableId, triggeredBy) {
|
|
const delayedItem = this.delayedItems.get(groupableId);
|
|
this.delayedItems.delete(groupableId);
|
|
const timeouts = this.delayedTimers.get(groupableId);
|
|
this.delayedTimers.delete(groupableId);
|
|
if (timeouts) {
|
|
for (const timeout of timeouts) {
|
|
clearTimeout(timeout);
|
|
}
|
|
}
|
|
if (delayedItem) {
|
|
const filterCount = delayedItem.subscriptions[0].filters.length;
|
|
const mergedFilters = [];
|
|
for (let i = 0; i < filterCount; i++) {
|
|
const allFiltersAtIndex = delayedItem.map((di) => di.filters[i]);
|
|
mergedFilters.push(mergeFilters(allFiltersAtIndex));
|
|
}
|
|
this.executeSubscriptions(groupableId, delayedItem, mergedFilters);
|
|
}
|
|
}
|
|
/**
|
|
* Executes one or more subscriptions.
|
|
*
|
|
* If the relay is not connected, subscriptions will be queued
|
|
* until the relay connects.
|
|
*
|
|
* @param groupableId
|
|
* @param subscriptionFilters
|
|
* @param mergedFilters
|
|
*/
|
|
executeSubscriptions(groupableId, groupedSubscriptions, mergedFilters) {
|
|
if (this.conn.isAvailable()) {
|
|
this.executeSubscriptionsConnected(groupableId, groupedSubscriptions, mergedFilters);
|
|
} else {
|
|
const connectedListener = () => {
|
|
this.debug("new relay coming online for active subscription", {
|
|
relay: this.ndkRelay.url,
|
|
mergeFilters
|
|
});
|
|
this.executeSubscriptionsConnected(
|
|
groupableId,
|
|
groupedSubscriptions,
|
|
mergedFilters
|
|
);
|
|
};
|
|
this.ndkRelay.once("connect", connectedListener);
|
|
groupedSubscriptions.once("close", () => {
|
|
this.ndkRelay.removeListener("connect", connectedListener);
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Executes one or more subscriptions.
|
|
*
|
|
* When there are more than one subscription, results
|
|
* will be sent to the right subscription
|
|
*
|
|
* @param subscriptions
|
|
* @param filters The filters as they should be sent to the relay
|
|
*/
|
|
executeSubscriptionsConnected(groupableId, groupedSubscriptions, mergedFilters) {
|
|
const subscriptions = [];
|
|
for (const { subscription } of groupedSubscriptions) {
|
|
subscriptions.push(subscription);
|
|
}
|
|
const subId = generateSubId(subscriptions, mergedFilters);
|
|
groupedSubscriptions.req = mergedFilters;
|
|
const sub = this.conn.relay.sub(mergedFilters, { id: subId });
|
|
this.activeSubscriptions.set(sub, groupedSubscriptions);
|
|
if (groupableId) {
|
|
this.activeSubscriptionsByGroupId.set(groupableId, { filters: mergedFilters, sub });
|
|
}
|
|
sub.on("event", (event) => {
|
|
const e = new NDKEvent(void 0, event);
|
|
e.relay = this.ndkRelay;
|
|
const subFilters = this.activeSubscriptions.get(sub);
|
|
subFilters?.eventReceived(e);
|
|
});
|
|
sub.on("eose", () => {
|
|
const subFilters = this.activeSubscriptions.get(sub);
|
|
subFilters?.eoseReceived(this.ndkRelay);
|
|
});
|
|
groupedSubscriptions.once("close", () => {
|
|
sub.unsub();
|
|
this.activeSubscriptions.delete(sub);
|
|
if (groupableId) {
|
|
this.activeSubscriptionsByGroupId.delete(groupableId);
|
|
}
|
|
});
|
|
return sub;
|
|
}
|
|
executedFilters() {
|
|
const ret = /* @__PURE__ */ new Map();
|
|
for (const [, groupedSubscriptions] of this.activeSubscriptions) {
|
|
ret.set(
|
|
groupedSubscriptions.req,
|
|
groupedSubscriptions.map((sub) => sub.subscription)
|
|
);
|
|
}
|
|
return ret;
|
|
}
|
|
};
|
|
|
|
// src/relay/index.ts
|
|
var NDKRelayStatus = /* @__PURE__ */ ((NDKRelayStatus2) => {
|
|
NDKRelayStatus2[NDKRelayStatus2["CONNECTING"] = 0] = "CONNECTING";
|
|
NDKRelayStatus2[NDKRelayStatus2["CONNECTED"] = 1] = "CONNECTED";
|
|
NDKRelayStatus2[NDKRelayStatus2["DISCONNECTING"] = 2] = "DISCONNECTING";
|
|
NDKRelayStatus2[NDKRelayStatus2["DISCONNECTED"] = 3] = "DISCONNECTED";
|
|
NDKRelayStatus2[NDKRelayStatus2["RECONNECTING"] = 4] = "RECONNECTING";
|
|
NDKRelayStatus2[NDKRelayStatus2["FLAPPING"] = 5] = "FLAPPING";
|
|
return NDKRelayStatus2;
|
|
})(NDKRelayStatus || {});
|
|
var NDKRelay = class extends import_eventemitter32.default {
|
|
url;
|
|
scores;
|
|
connectivity;
|
|
subs;
|
|
publisher;
|
|
complaining = false;
|
|
debug;
|
|
constructor(url) {
|
|
super();
|
|
this.url = url;
|
|
this.scores = /* @__PURE__ */ new Map();
|
|
this.debug = (0, import_debug.default)(`ndk:relay:${url}`);
|
|
this.connectivity = new NDKRelayConnectivity(this);
|
|
this.subs = new NDKRelaySubscriptions(this);
|
|
this.publisher = new NDKRelayPublisher(this);
|
|
}
|
|
get status() {
|
|
return this.connectivity.status;
|
|
}
|
|
get connectionStats() {
|
|
return this.connectivity.connectionStats;
|
|
}
|
|
/**
|
|
* Connects to the relay.
|
|
*/
|
|
async connect() {
|
|
return this.connectivity.connect();
|
|
}
|
|
/**
|
|
* Disconnects from the relay.
|
|
*/
|
|
disconnect() {
|
|
this.connectivity.disconnect();
|
|
}
|
|
/**
|
|
* Queues or executes the subscription of a specific set of filters
|
|
* within this relay.
|
|
*
|
|
* @param subscription NDKSubscription this filters belong to.
|
|
* @param filters Filters to execute
|
|
*/
|
|
subscribe(subscription, filters) {
|
|
this.subs.subscribe(subscription, filters);
|
|
}
|
|
/**
|
|
* Publishes an event to the relay with an optional timeout.
|
|
*
|
|
* If the relay is not connected, the event will be published when the relay connects,
|
|
* unless the timeout is reached before the relay connects.
|
|
*
|
|
* @param event The event to publish
|
|
* @param timeoutMs The timeout for the publish operation in milliseconds
|
|
* @returns A promise that resolves when the event has been published or rejects if the operation times out
|
|
*/
|
|
async publish(event, timeoutMs = 2500) {
|
|
return this.publisher.publish(event, timeoutMs);
|
|
}
|
|
/**
|
|
* Called when this relay has responded with an event but
|
|
* wasn't the fastest one.
|
|
* @param timeDiffInMs The time difference in ms between the fastest and this relay in milliseconds
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
scoreSlowerEvent(timeDiffInMs) {
|
|
}
|
|
/** @deprecated Use referenceTags instead. */
|
|
tagReference(marker) {
|
|
const tag = ["r", this.url];
|
|
if (marker) {
|
|
tag.push(marker);
|
|
}
|
|
return tag;
|
|
}
|
|
referenceTags() {
|
|
return [["r", this.url]];
|
|
}
|
|
activeSubscriptions() {
|
|
return this.subs.executedFilters();
|
|
}
|
|
};
|
|
|
|
// src/relay/sets/index.ts
|
|
var NDKRelaySet = class {
|
|
relays;
|
|
debug;
|
|
ndk;
|
|
constructor(relays, ndk) {
|
|
this.relays = relays;
|
|
this.ndk = ndk;
|
|
this.debug = ndk.debug.extend("relayset");
|
|
}
|
|
/**
|
|
* Adds a relay to this set.
|
|
*/
|
|
addRelay(relay) {
|
|
this.relays.add(relay);
|
|
}
|
|
/**
|
|
* Creates a relay set from a list of relay URLs.
|
|
*
|
|
* If no connection to the relay is found in the pool it will temporarily
|
|
* connect to it.
|
|
*
|
|
* @param relayUrls - list of relay URLs to include in this set
|
|
* @param ndk
|
|
* @returns NDKRelaySet
|
|
*/
|
|
static fromRelayUrls(relayUrls, ndk) {
|
|
const relays = /* @__PURE__ */ new Set();
|
|
for (const url of relayUrls) {
|
|
const relay = ndk.pool.relays.get(url);
|
|
if (relay) {
|
|
relays.add(relay);
|
|
} else {
|
|
const temporaryRelay = new NDKRelay(url);
|
|
ndk.pool.useTemporaryRelay(temporaryRelay);
|
|
relays.add(temporaryRelay);
|
|
}
|
|
}
|
|
return new NDKRelaySet(new Set(relays), ndk);
|
|
}
|
|
/**
|
|
* Publish an event to all relays in this set. Returns the number of relays that have received the event.
|
|
* @param event
|
|
* @param timeoutMs - timeout in milliseconds for each publish operation and connection operation
|
|
* @returns A set where the event was successfully published to
|
|
*/
|
|
async publish(event, timeoutMs) {
|
|
const publishedToRelays = /* @__PURE__ */ new Set();
|
|
const isEphemeral2 = event.isEphemeral();
|
|
const promises = Array.from(this.relays).map((relay) => {
|
|
return new Promise((resolve) => {
|
|
relay.publish(event, timeoutMs).then(() => {
|
|
publishedToRelays.add(relay);
|
|
resolve();
|
|
}).catch((err) => {
|
|
if (!isEphemeral2) {
|
|
this.debug("error publishing to relay", {
|
|
relay: relay.url,
|
|
err
|
|
});
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
await Promise.all(promises);
|
|
if (publishedToRelays.size === 0) {
|
|
if (!isEphemeral2) {
|
|
throw new Error("No relay was able to receive the event");
|
|
}
|
|
}
|
|
return publishedToRelays;
|
|
}
|
|
size() {
|
|
return this.relays.size;
|
|
}
|
|
};
|
|
|
|
// src/relay/sets/calculate.ts
|
|
function calculateRelaySetFromEvent(ndk, event) {
|
|
const relays = /* @__PURE__ */ new Set();
|
|
ndk.pool?.relays.forEach((relay) => relays.add(relay));
|
|
return new NDKRelaySet(relays, ndk);
|
|
}
|
|
function getWriteRelaysFor(ndk, author) {
|
|
if (!ndk.outboxTracker)
|
|
return void 0;
|
|
return ndk.outboxTracker.data.get(author)?.writeRelays;
|
|
}
|
|
function calculateRelaySetsFromFilter(ndk, filters) {
|
|
const result = /* @__PURE__ */ new Map();
|
|
const authors = /* @__PURE__ */ new Set();
|
|
filters.forEach((filter) => {
|
|
if (filter.authors) {
|
|
filter.authors.forEach((author) => authors.add(author));
|
|
}
|
|
});
|
|
if (authors.size > 0) {
|
|
const authorToRelaysMap = /* @__PURE__ */ new Map();
|
|
for (const author of authors) {
|
|
const userWriteRelays = getWriteRelaysFor(ndk, author);
|
|
if (userWriteRelays && userWriteRelays.size > 0) {
|
|
ndk.debug(`Adding ${userWriteRelays.size} relays for ${author}`);
|
|
userWriteRelays.forEach((relay) => {
|
|
const authorsInRelay = authorToRelaysMap.get(relay) || [];
|
|
authorsInRelay.push(author);
|
|
authorToRelaysMap.set(relay, authorsInRelay);
|
|
});
|
|
} else {
|
|
ndk.explicitRelayUrls?.forEach((relay) => {
|
|
const authorsInRelay = authorToRelaysMap.get(relay) || [];
|
|
authorsInRelay.push(author);
|
|
authorToRelaysMap.set(relay, authorsInRelay);
|
|
});
|
|
}
|
|
}
|
|
for (const relayUrl of authorToRelaysMap.keys()) {
|
|
result.set(relayUrl, []);
|
|
}
|
|
for (const filter of filters) {
|
|
if (filter.authors) {
|
|
for (const [relayUrl, authors2] of authorToRelaysMap.entries()) {
|
|
result.set(relayUrl, [...result.get(relayUrl), { ...filter, authors: authors2 }]);
|
|
}
|
|
} else {
|
|
for (const relayUrl of authorToRelaysMap.keys()) {
|
|
result.set(relayUrl, [...result.get(relayUrl), filter]);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
ndk.explicitRelayUrls?.forEach((relay) => {
|
|
result.set(relay, filters);
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
function calculateRelaySetsFromFilters(ndk, filters) {
|
|
return calculateRelaySetsFromFilter(ndk, filters);
|
|
}
|
|
|
|
// src/zap/index.ts
|
|
var import_base = require("@scure/base");
|
|
var import_eventemitter33 = __toESM(require("eventemitter3"));
|
|
var import_nostr_tools4 = require("nostr-tools");
|
|
var DEFAULT_RELAYS = [
|
|
"wss://nos.lol",
|
|
"wss://relay.nostr.band",
|
|
"wss://relay.f7z.io",
|
|
"wss://relay.damus.io",
|
|
"wss://nostr.mom",
|
|
"wss://no.str.cr"
|
|
];
|
|
var Zap = class extends import_eventemitter33.default {
|
|
ndk;
|
|
zappedEvent;
|
|
zappedUser;
|
|
constructor(args) {
|
|
super();
|
|
this.ndk = args.ndk;
|
|
this.zappedEvent = args.zappedEvent;
|
|
this.zappedUser = args.zappedUser || this.ndk.getUser({ hexpubkey: this.zappedEvent?.pubkey });
|
|
}
|
|
async getZapEndpoint() {
|
|
let lud06;
|
|
let lud16;
|
|
let zapEndpoint;
|
|
let zapEndpointCallback;
|
|
if (this.zappedUser) {
|
|
if (!this.zappedUser.profile) {
|
|
await this.zappedUser.fetchProfile();
|
|
}
|
|
lud06 = (this.zappedUser.profile || {}).lud06;
|
|
lud16 = (this.zappedUser.profile || {}).lud16;
|
|
}
|
|
if (lud16 && !lud16.startsWith("LNURL")) {
|
|
const [name, domain] = lud16.split("@");
|
|
zapEndpoint = `https://${domain}/.well-known/lnurlp/${name}`;
|
|
} else if (lud06) {
|
|
const { words } = import_base.bech32.decode(lud06, 1e3);
|
|
const data = import_base.bech32.fromWords(words);
|
|
const utf8Decoder = new TextDecoder("utf-8");
|
|
zapEndpoint = utf8Decoder.decode(data);
|
|
}
|
|
if (!zapEndpoint) {
|
|
throw new Error("No zap endpoint found");
|
|
}
|
|
const response = await fetch(zapEndpoint);
|
|
const body = await response.json();
|
|
if (body?.allowsNostr && (body?.nostrPubkey || body?.nostrPubKey)) {
|
|
zapEndpointCallback = body.callback;
|
|
}
|
|
return zapEndpointCallback;
|
|
}
|
|
/**
|
|
* Generates a kind:9734 zap request and returns the payment request
|
|
* @param amount amount to zap in millisatoshis
|
|
* @param comment optional comment to include in the zap request
|
|
* @param extraTags optional extra tags to include in the zap request
|
|
* @param relays optional relays to ask zapper to publish the zap to
|
|
* @returns the payment request
|
|
*/
|
|
async createZapRequest(amount, comment, extraTags, relays, signer) {
|
|
const zapEndpoint = await this.getZapEndpoint();
|
|
if (!zapEndpoint) {
|
|
throw new Error("No zap endpoint found");
|
|
}
|
|
if (!this.zappedEvent && !this.zappedUser)
|
|
throw new Error("No zapped event or user found");
|
|
const zapRequest = import_nostr_tools4.nip57.makeZapRequest({
|
|
profile: this.zappedUser.hexpubkey,
|
|
// set the event to null since nostr-tools doesn't support nip-33 zaps
|
|
event: null,
|
|
amount,
|
|
comment: comment || "",
|
|
relays: relays ?? this.relays()
|
|
});
|
|
if (this.zappedEvent) {
|
|
const tags = this.zappedEvent.referenceTags();
|
|
zapRequest.tags.push(...tags);
|
|
}
|
|
zapRequest.tags.push(["lnurl", zapEndpoint]);
|
|
const zapRequestEvent = new NDKEvent(this.ndk, zapRequest);
|
|
if (extraTags) {
|
|
zapRequestEvent.tags = zapRequestEvent.tags.concat(extraTags);
|
|
}
|
|
await zapRequestEvent.sign(signer);
|
|
const zapRequestNostrEvent = await zapRequestEvent.toNostrEvent();
|
|
const response = await fetch(
|
|
`${zapEndpoint}?` + new URLSearchParams({
|
|
amount: amount.toString(),
|
|
nostr: JSON.stringify(zapRequestNostrEvent)
|
|
})
|
|
);
|
|
const body = await response.json();
|
|
return body.pr;
|
|
}
|
|
/**
|
|
* @returns the relays to use for the zap request
|
|
*/
|
|
relays() {
|
|
let r = [];
|
|
if (this.ndk?.pool?.relays) {
|
|
r = this.ndk.pool.urls();
|
|
}
|
|
if (!r.length) {
|
|
r = DEFAULT_RELAYS;
|
|
}
|
|
return r;
|
|
}
|
|
};
|
|
|
|
// src/events/content-tagger.ts
|
|
var import_nostr_tools5 = require("nostr-tools");
|
|
async function generateContentTags(content, tags = []) {
|
|
const tagRegex = /(@|nostr:)(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]+/g;
|
|
const hashtagRegex = /#(\w+)/g;
|
|
let promises = [];
|
|
const addTagIfNew = (t) => {
|
|
if (!tags.find((t2) => t2[0] === t[0] && t2[1] === t[1])) {
|
|
tags.push(t);
|
|
}
|
|
};
|
|
content = content.replace(tagRegex, (tag) => {
|
|
try {
|
|
const entity = tag.split(/(@|nostr:)/)[2];
|
|
const { type, data } = import_nostr_tools5.nip19.decode(entity);
|
|
let t;
|
|
switch (type) {
|
|
case "npub":
|
|
t = ["p", data];
|
|
break;
|
|
case "nprofile":
|
|
t = ["p", data.pubkey];
|
|
break;
|
|
case "note":
|
|
promises.push(
|
|
new Promise(async (resolve) => {
|
|
addTagIfNew([
|
|
"e",
|
|
data,
|
|
await maybeGetEventRelayUrl(entity),
|
|
"mention"
|
|
]);
|
|
resolve();
|
|
})
|
|
);
|
|
break;
|
|
case "nevent":
|
|
promises.push(
|
|
new Promise(async (resolve) => {
|
|
let { id, relays, author } = data;
|
|
if (!relays || relays.length === 0) {
|
|
relays = [await maybeGetEventRelayUrl(entity)];
|
|
}
|
|
addTagIfNew(["e", id, relays[0], "mention"]);
|
|
if (author)
|
|
addTagIfNew(["p", author]);
|
|
resolve();
|
|
})
|
|
);
|
|
break;
|
|
case "naddr":
|
|
promises.push(
|
|
new Promise(async (resolve) => {
|
|
const id = [data.kind, data.pubkey, data.identifier].join(":");
|
|
let relays = data.relays ?? [];
|
|
if (relays.length === 0) {
|
|
relays = [await maybeGetEventRelayUrl(entity)];
|
|
}
|
|
addTagIfNew(["a", id, relays[0], "mention"]);
|
|
addTagIfNew(["p", data.pubkey]);
|
|
resolve();
|
|
})
|
|
);
|
|
break;
|
|
default:
|
|
return tag;
|
|
}
|
|
if (t)
|
|
addTagIfNew(t);
|
|
return `nostr:${entity}`;
|
|
} catch (error) {
|
|
return tag;
|
|
}
|
|
});
|
|
await Promise.all(promises);
|
|
content = content.replace(hashtagRegex, (tag, word) => {
|
|
const t = ["t", word];
|
|
if (!tags.find((t2) => t2[0] === t[0] && t2[1] === t[1])) {
|
|
tags.push(t);
|
|
}
|
|
return tag;
|
|
});
|
|
return { content, tags };
|
|
}
|
|
async function maybeGetEventRelayUrl(nip19Id) {
|
|
return "";
|
|
}
|
|
|
|
// src/events/kind.ts
|
|
function isReplaceable() {
|
|
if (this.kind === void 0)
|
|
throw new Error("Kind not set");
|
|
return this.kind >= 1e4 && this.kind < 2e4;
|
|
}
|
|
function isEphemeral() {
|
|
if (this.kind === void 0)
|
|
throw new Error("Kind not set");
|
|
return this.kind >= 2e4 && this.kind < 3e4;
|
|
}
|
|
function isParamReplaceable() {
|
|
if (this.kind === void 0)
|
|
throw new Error("Kind not set");
|
|
return this.kind >= 3e4 && this.kind < 4e4;
|
|
}
|
|
|
|
// src/events/kinds/index.ts
|
|
var NDKKind = /* @__PURE__ */ ((NDKKind2) => {
|
|
NDKKind2[NDKKind2["Metadata"] = 0] = "Metadata";
|
|
NDKKind2[NDKKind2["Text"] = 1] = "Text";
|
|
NDKKind2[NDKKind2["RecommendRelay"] = 2] = "RecommendRelay";
|
|
NDKKind2[NDKKind2["Contacts"] = 3] = "Contacts";
|
|
NDKKind2[NDKKind2["EncryptedDirectMessage"] = 4] = "EncryptedDirectMessage";
|
|
NDKKind2[NDKKind2["EventDeletion"] = 5] = "EventDeletion";
|
|
NDKKind2[NDKKind2["Repost"] = 6] = "Repost";
|
|
NDKKind2[NDKKind2["Reaction"] = 7] = "Reaction";
|
|
NDKKind2[NDKKind2["BadgeAward"] = 8] = "BadgeAward";
|
|
NDKKind2[NDKKind2["GenericRepost"] = 16] = "GenericRepost";
|
|
NDKKind2[NDKKind2["ChannelCreation"] = 40] = "ChannelCreation";
|
|
NDKKind2[NDKKind2["ChannelMetadata"] = 41] = "ChannelMetadata";
|
|
NDKKind2[NDKKind2["ChannelMessage"] = 42] = "ChannelMessage";
|
|
NDKKind2[NDKKind2["ChannelHideMessage"] = 43] = "ChannelHideMessage";
|
|
NDKKind2[NDKKind2["ChannelMuteUser"] = 44] = "ChannelMuteUser";
|
|
NDKKind2[NDKKind2["Report"] = 1984] = "Report";
|
|
NDKKind2[NDKKind2["Label"] = 1985] = "Label";
|
|
NDKKind2[NDKKind2["DVMReqTextExtraction"] = 5e3] = "DVMReqTextExtraction";
|
|
NDKKind2[NDKKind2["DVMReqTextSummarization"] = 5001] = "DVMReqTextSummarization";
|
|
NDKKind2[NDKKind2["DVMReqTextTranslation"] = 5002] = "DVMReqTextTranslation";
|
|
NDKKind2[NDKKind2["DVMReqTextGeneration"] = 5050] = "DVMReqTextGeneration";
|
|
NDKKind2[NDKKind2["DVMReqImageGeneration"] = 5100] = "DVMReqImageGeneration";
|
|
NDKKind2[NDKKind2["DVMReqDiscoveryNostrContent"] = 5300] = "DVMReqDiscoveryNostrContent";
|
|
NDKKind2[NDKKind2["DVMReqDiscoveryNostrPeople"] = 5301] = "DVMReqDiscoveryNostrPeople";
|
|
NDKKind2[NDKKind2["DVMJobFeedback"] = 7e3] = "DVMJobFeedback";
|
|
NDKKind2[NDKKind2["ZapRequest"] = 9734] = "ZapRequest";
|
|
NDKKind2[NDKKind2["Zap"] = 9735] = "Zap";
|
|
NDKKind2[NDKKind2["Highlight"] = 9802] = "Highlight";
|
|
NDKKind2[NDKKind2["MuteList"] = 1e4] = "MuteList";
|
|
NDKKind2[NDKKind2["PinList"] = 10001] = "PinList";
|
|
NDKKind2[NDKKind2["RelayList"] = 10002] = "RelayList";
|
|
NDKKind2[NDKKind2["ClientAuth"] = 22242] = "ClientAuth";
|
|
NDKKind2[NDKKind2["NostrConnect"] = 24133] = "NostrConnect";
|
|
NDKKind2[NDKKind2["CategorizedPeopleList"] = 3e4] = "CategorizedPeopleList";
|
|
NDKKind2[NDKKind2["CategorizedBookmarkList"] = 30001] = "CategorizedBookmarkList";
|
|
NDKKind2[NDKKind2["ProfileBadge"] = 30008] = "ProfileBadge";
|
|
NDKKind2[NDKKind2["BadgeDefinition"] = 30009] = "BadgeDefinition";
|
|
NDKKind2[NDKKind2["InterestsList"] = 30015] = "InterestsList";
|
|
NDKKind2[NDKKind2["MarketStall"] = 30017] = "MarketStall";
|
|
NDKKind2[NDKKind2["MarketProduct"] = 30018] = "MarketProduct";
|
|
NDKKind2[NDKKind2["CategorizedRelayList"] = 30022] = "CategorizedRelayList";
|
|
NDKKind2[NDKKind2["Article"] = 30023] = "Article";
|
|
NDKKind2[NDKKind2["AppSpecificData"] = 30078] = "AppSpecificData";
|
|
NDKKind2[NDKKind2["Classified"] = 30402] = "Classified";
|
|
NDKKind2[NDKKind2["AppRecommendation"] = 31989] = "AppRecommendation";
|
|
NDKKind2[NDKKind2["AppHandler"] = 31990] = "AppHandler";
|
|
NDKKind2[NDKKind2["CategorizedHighlightList"] = 39802] = "CategorizedHighlightList";
|
|
return NDKKind2;
|
|
})(NDKKind || {});
|
|
var NDKListKinds = [
|
|
3e4 /* CategorizedPeopleList */,
|
|
30001 /* CategorizedBookmarkList */,
|
|
39802 /* CategorizedHighlightList */,
|
|
30022 /* CategorizedRelayList */,
|
|
30015 /* InterestsList */
|
|
];
|
|
|
|
// src/events/nip04.ts
|
|
async function encrypt(recipient, signer) {
|
|
if (!this.ndk)
|
|
throw new Error("No NDK instance found!");
|
|
if (!signer) {
|
|
await this.ndk.assertSigner();
|
|
signer = this.ndk.signer;
|
|
}
|
|
if (!recipient) {
|
|
const pTags = this.getMatchingTags("p");
|
|
if (pTags.length !== 1) {
|
|
throw new Error(
|
|
"No recipient could be determined and no explicit recipient was provided"
|
|
);
|
|
}
|
|
recipient = this.ndk.getUser({ hexpubkey: pTags[0][1] });
|
|
}
|
|
this.content = await signer?.encrypt(recipient, this.content);
|
|
}
|
|
async function decrypt(sender, signer) {
|
|
if (!this.ndk)
|
|
throw new Error("No NDK instance found!");
|
|
if (!signer) {
|
|
await this.ndk.assertSigner();
|
|
signer = this.ndk.signer;
|
|
}
|
|
if (!sender) {
|
|
sender = this.author;
|
|
}
|
|
this.content = await signer?.decrypt(sender, this.content);
|
|
}
|
|
|
|
// src/events/nip19.ts
|
|
var import_nostr_tools6 = require("nostr-tools");
|
|
function encode() {
|
|
if (this.isParamReplaceable()) {
|
|
return import_nostr_tools6.nip19.naddrEncode({
|
|
kind: this.kind,
|
|
pubkey: this.pubkey,
|
|
identifier: this.replaceableDTag(),
|
|
relays: this.relay ? [this.relay.url] : []
|
|
});
|
|
} else if (this.relay) {
|
|
return import_nostr_tools6.nip19.neventEncode({
|
|
id: this.tagId(),
|
|
relays: [this.relay.url],
|
|
author: this.pubkey
|
|
});
|
|
} else {
|
|
return import_nostr_tools6.nip19.noteEncode(this.tagId());
|
|
}
|
|
}
|
|
|
|
// src/events/repost.ts
|
|
async function repost(publish = true, signer) {
|
|
if (!signer && publish) {
|
|
if (!this.ndk)
|
|
throw new Error("No NDK instance found");
|
|
this.ndk.assertSigner();
|
|
signer = this.ndk.signer;
|
|
}
|
|
const e = new NDKEvent(this.ndk, {
|
|
kind: getKind(this),
|
|
content: ""
|
|
});
|
|
e.tag(this);
|
|
if (e.kind === 16 /* GenericRepost */) {
|
|
e.tags.push(["k", `${this.kind}`]);
|
|
}
|
|
if (signer)
|
|
await e.sign(signer);
|
|
if (publish)
|
|
await e.publish();
|
|
return e;
|
|
}
|
|
function getKind(event) {
|
|
if (event.kind === 1) {
|
|
return 6 /* Repost */;
|
|
}
|
|
return 16 /* GenericRepost */;
|
|
}
|
|
|
|
// src/events/index.ts
|
|
var NDKEvent = class extends import_eventemitter34.default {
|
|
ndk;
|
|
created_at;
|
|
content = "";
|
|
tags = [];
|
|
kind;
|
|
id = "";
|
|
sig;
|
|
pubkey = "";
|
|
_author = void 0;
|
|
/**
|
|
* The relay that this event was first received from.
|
|
*/
|
|
relay;
|
|
constructor(ndk, event) {
|
|
super();
|
|
this.ndk = ndk;
|
|
this.created_at = event?.created_at;
|
|
this.content = event?.content || "";
|
|
this.tags = event?.tags || [];
|
|
this.id = event?.id || "";
|
|
this.sig = event?.sig;
|
|
this.pubkey = event?.pubkey || "";
|
|
this.kind = event?.kind;
|
|
}
|
|
/**
|
|
* Returns the event as is.
|
|
*/
|
|
rawEvent() {
|
|
return {
|
|
created_at: this.created_at,
|
|
content: this.content,
|
|
tags: this.tags,
|
|
kind: this.kind,
|
|
pubkey: this.pubkey,
|
|
id: this.id,
|
|
sig: this.sig
|
|
};
|
|
}
|
|
set author(user) {
|
|
this.pubkey = user.hexpubkey;
|
|
this._author = void 0;
|
|
}
|
|
/**
|
|
* Returns an NDKUser for the author of the event.
|
|
*/
|
|
get author() {
|
|
if (this._author)
|
|
return this._author;
|
|
if (!this.ndk)
|
|
throw new Error("No NDK instance found");
|
|
const user = this.ndk.getUser({ hexpubkey: this.pubkey });
|
|
this._author = user;
|
|
return user;
|
|
}
|
|
tag(userOrEvent, marker) {
|
|
const tags = userOrEvent.referenceTags();
|
|
if (marker)
|
|
tags[0].push(marker);
|
|
this.tags.push(...tags);
|
|
if (userOrEvent instanceof NDKEvent) {
|
|
const tagEventAuthor = userOrEvent.author;
|
|
if (tagEventAuthor && this.pubkey !== tagEventAuthor.hexpubkey) {
|
|
this.tag(tagEventAuthor);
|
|
}
|
|
for (const pTag of userOrEvent.getMatchingTags("p")) {
|
|
if (pTag[1] === this.pubkey)
|
|
continue;
|
|
if (this.tags.find((t) => t[0] === "p" && t[1] === pTag[1]))
|
|
continue;
|
|
this.tags.push(["p", pTag[1]]);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Return a NostrEvent object, trying to fill in missing fields
|
|
* when possible, adding tags when necessary.
|
|
* @param pubkey {string} The pubkey of the user who the event belongs to.
|
|
* @returns {Promise<NostrEvent>} A promise that resolves to a NostrEvent.
|
|
*/
|
|
async toNostrEvent(pubkey) {
|
|
if (!pubkey && this.pubkey === "") {
|
|
const user = await this.ndk?.signer?.user();
|
|
this.pubkey = user?.hexpubkey || "";
|
|
}
|
|
if (!this.created_at)
|
|
this.created_at = Math.floor(Date.now() / 1e3);
|
|
const nostrEvent = this.rawEvent();
|
|
const { content, tags } = await this.generateTags();
|
|
nostrEvent.content = content || "";
|
|
nostrEvent.tags = tags;
|
|
try {
|
|
this.id = (0, import_nostr_tools7.getEventHash)(nostrEvent);
|
|
} catch (e) {
|
|
}
|
|
if (this.id)
|
|
nostrEvent.id = this.id;
|
|
if (this.sig)
|
|
nostrEvent.sig = this.sig;
|
|
return nostrEvent;
|
|
}
|
|
isReplaceable = isReplaceable.bind(this);
|
|
isEphemeral = isEphemeral.bind(this);
|
|
isParamReplaceable = isParamReplaceable.bind(this);
|
|
/**
|
|
* Encodes a bech32 id.
|
|
*
|
|
* @returns {string} - Encoded naddr, note or nevent.
|
|
*/
|
|
encode = encode.bind(this);
|
|
encrypt = encrypt.bind(this);
|
|
decrypt = decrypt.bind(this);
|
|
/**
|
|
* Get all tags with the given name
|
|
* @param tagName {string} The name of the tag to search for
|
|
* @returns {NDKTag[]} An array of the matching tags
|
|
*/
|
|
getMatchingTags(tagName) {
|
|
return this.tags.filter((tag) => tag[0] === tagName);
|
|
}
|
|
/**
|
|
* Get the first tag with the given name
|
|
* @param tagName Tag name to search for
|
|
* @returns The value of the first tag with the given name, or undefined if no such tag exists
|
|
*/
|
|
tagValue(tagName) {
|
|
const tags = this.getMatchingTags(tagName);
|
|
if (tags.length === 0)
|
|
return void 0;
|
|
return tags[0][1];
|
|
}
|
|
/**
|
|
* Remove all tags with the given name (e.g. "d", "a", "p")
|
|
* @param tagName Tag name to search for and remove
|
|
* @returns {void}
|
|
*/
|
|
removeTag(tagName) {
|
|
this.tags = this.tags.filter((tag) => tag[0] !== tagName);
|
|
}
|
|
/**
|
|
* Sign the event if a signer is present.
|
|
*
|
|
* It will generate tags.
|
|
* Repleacable events will have their created_at field set to the current time.
|
|
* @param signer {NDKSigner} The NDKSigner to use to sign the event
|
|
* @returns {Promise<string>} A Promise that resolves to the signature of the signed event.
|
|
*/
|
|
async sign(signer) {
|
|
if (!signer) {
|
|
this.ndk?.assertSigner();
|
|
signer = this.ndk.signer;
|
|
} else {
|
|
this.author = await signer.user();
|
|
}
|
|
await this.generateTags();
|
|
if (this.isReplaceable()) {
|
|
this.created_at = Math.floor(Date.now() / 1e3);
|
|
}
|
|
const nostrEvent = await this.toNostrEvent();
|
|
this.sig = await signer.sign(nostrEvent);
|
|
return this.sig;
|
|
}
|
|
/**
|
|
* Attempt to sign and then publish an NDKEvent to a given relaySet.
|
|
* If no relaySet is provided, the relaySet will be calculated by NDK.
|
|
* @param relaySet {NDKRelaySet} The relaySet to publish the even to.
|
|
* @returns A promise that resolves to the relays the event was published to.
|
|
*/
|
|
async publish(relaySet, timeoutMs) {
|
|
if (!this.sig)
|
|
await this.sign();
|
|
if (!this.ndk)
|
|
throw new Error("NDKEvent must be associated with an NDK instance to publish");
|
|
if (!relaySet) {
|
|
relaySet = this.ndk.devWriteRelaySet || calculateRelaySetFromEvent(this.ndk, this);
|
|
}
|
|
return relaySet.publish(this, timeoutMs);
|
|
}
|
|
/**
|
|
* Generates tags for users, notes, and other events tagged in content.
|
|
* Will also generate random "d" tag for parameterized replaceable events where needed.
|
|
* @returns {ContentTag} The tags and content of the event.
|
|
*/
|
|
async generateTags() {
|
|
let tags = [];
|
|
const g = await generateContentTags(this.content, this.tags);
|
|
const content = g.content;
|
|
tags = g.tags;
|
|
if (this.kind && this.isParamReplaceable()) {
|
|
const dTag = this.getMatchingTags("d")[0];
|
|
if (!dTag) {
|
|
const str = [...Array(16)].map(() => Math.random().toString(36)[2]).join("");
|
|
tags.push(["d", str]);
|
|
}
|
|
}
|
|
return { content: content || "", tags };
|
|
}
|
|
muted() {
|
|
const authorMutedEntry = this.ndk?.mutedIds.get(this.pubkey);
|
|
if (authorMutedEntry && authorMutedEntry === "p")
|
|
return "author";
|
|
const eventTagReference = this.tagReference();
|
|
const eventMutedEntry = this.ndk?.mutedIds.get(eventTagReference[1]);
|
|
if (eventMutedEntry && eventMutedEntry === eventTagReference[0])
|
|
return "event";
|
|
return null;
|
|
}
|
|
/**
|
|
* Returns the "d" tag of a parameterized replaceable event or throws an error if the event isn't
|
|
* a parameterized replaceable event.
|
|
* @returns {string} the "d" tag of the event.
|
|
*/
|
|
replaceableDTag() {
|
|
if (this.kind && this.kind >= 3e4 && this.kind <= 4e4) {
|
|
const dTag = this.getMatchingTags("d")[0];
|
|
const dTagId = dTag ? dTag[1] : "";
|
|
return dTagId;
|
|
}
|
|
throw new Error("Event is not a parameterized replaceable event");
|
|
}
|
|
/**
|
|
* 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
|
|
*/
|
|
deduplicationKey() {
|
|
if (this.kind === 0 || this.kind === 3 || this.kind && this.kind >= 1e4 && this.kind < 2e4) {
|
|
return `${this.kind}:${this.pubkey}`;
|
|
} else {
|
|
return this.tagId();
|
|
}
|
|
}
|
|
/**
|
|
* Returns the id of the event or, if it's a parameterized event, the generated id of the event using "d" tag, pubkey, and kind.
|
|
* @returns {string} The id
|
|
*/
|
|
tagId() {
|
|
if (this.isParamReplaceable()) {
|
|
return this.tagAddress();
|
|
}
|
|
return this.id;
|
|
}
|
|
/**
|
|
* Returns the "reference" value ("<kind>:<author-pubkey>:<d-tag>") for this replaceable event.
|
|
* @returns {string} The id
|
|
*/
|
|
tagAddress() {
|
|
if (!this.isParamReplaceable()) {
|
|
throw new Error("This must only be called on replaceable events");
|
|
}
|
|
const dTagId = this.replaceableDTag();
|
|
return `${this.kind}:${this.pubkey}:${dTagId}`;
|
|
}
|
|
/** @deprecated Use referenceTags instead. */
|
|
/**
|
|
* Get the tag that can be used to reference this event from another event
|
|
* @example
|
|
* event = new NDKEvent(ndk, { kind: 30000, pubkey: 'pubkey', tags: [ ["d", "d-code"] ] });
|
|
* event.tagReference(); // ["a", "30000:pubkey:d-code"]
|
|
*
|
|
* event = new NDKEvent(ndk, { kind: 1, pubkey: 'pubkey', id: "eventid" });
|
|
* event.tagReference(); // ["e", "eventid"]
|
|
* @returns {NDKTag} The NDKTag object referencing this event
|
|
*/
|
|
tagReference() {
|
|
if (this.isParamReplaceable()) {
|
|
return ["a", this.tagAddress()];
|
|
}
|
|
return ["e", this.tagId()];
|
|
}
|
|
/**
|
|
* Get the tags that can be used to reference this event from another event
|
|
* @param marker The marker to use in the tag
|
|
* @example
|
|
* event = new NDKEvent(ndk, { kind: 30000, pubkey: 'pubkey', tags: [ ["d", "d-code"] ] });
|
|
* event.referenceTags(); // [["a", "30000:pubkey:d-code"], ["e", "parent-id"]]
|
|
*
|
|
* event = new NDKEvent(ndk, { kind: 1, pubkey: 'pubkey', id: "eventid" });
|
|
* event.referenceTags(); // [["e", "parent-id"]]
|
|
* @returns {NDKTag} The NDKTag object referencing this event
|
|
*/
|
|
referenceTags(marker) {
|
|
let tags = [];
|
|
if (this.isParamReplaceable()) {
|
|
tags = [
|
|
["a", this.tagAddress()],
|
|
["e", this.id]
|
|
];
|
|
} else {
|
|
tags = [["e", this.id]];
|
|
}
|
|
if (marker) {
|
|
tags.forEach((tag) => tag.push(marker));
|
|
}
|
|
tags.push(...this.author.referenceTags());
|
|
return tags;
|
|
}
|
|
/**
|
|
* Provides the filter that will return matching events for this event.
|
|
*
|
|
* @example
|
|
* event = new NDKEvent(ndk, { kind: 30000, pubkey: 'pubkey', tags: [ ["d", "d-code"] ] });
|
|
* event.filter(); // { "#a": ["30000:pubkey:d-code"] }
|
|
* @example
|
|
* event = new NDKEvent(ndk, { kind: 1, pubkey: 'pubkey', id: "eventid" });
|
|
* event.filter(); // { "#e": ["eventid"] }
|
|
*
|
|
* @returns The filter that will return matching events for this event
|
|
*/
|
|
filter() {
|
|
if (this.isParamReplaceable()) {
|
|
return { "#a": [this.tagId()] };
|
|
} else {
|
|
return { "#e": [this.tagId()] };
|
|
}
|
|
}
|
|
/**
|
|
* Create a zap request for an existing event
|
|
*
|
|
* @param amount The amount to zap in millisatoshis
|
|
* @param comment A comment to add to the zap request
|
|
* @param extraTags Extra tags to add to the zap request
|
|
* @param recipient The zap recipient (optional for events)
|
|
* @param signer The signer to use (will default to the NDK instance's signer)
|
|
*/
|
|
async zap(amount, comment, extraTags, recipient, signer) {
|
|
if (!this.ndk)
|
|
throw new Error("No NDK instance found");
|
|
if (!signer) {
|
|
this.ndk.assertSigner();
|
|
}
|
|
const zap = new Zap({
|
|
ndk: this.ndk,
|
|
zappedEvent: this,
|
|
zappedUser: recipient
|
|
});
|
|
const relays = Array.from(this.ndk.pool.relays.keys());
|
|
const paymentRequest = await zap.createZapRequest(
|
|
amount,
|
|
comment,
|
|
extraTags,
|
|
relays,
|
|
signer
|
|
);
|
|
return paymentRequest;
|
|
}
|
|
/**
|
|
* Generates a deletion event of the current event
|
|
*
|
|
* @param reason The reason for the deletion
|
|
* @returns The deletion event
|
|
*/
|
|
async delete(reason) {
|
|
if (!this.ndk)
|
|
throw new Error("No NDK instance found");
|
|
this.ndk.assertSigner();
|
|
const e = new NDKEvent(this.ndk, {
|
|
kind: 5 /* EventDeletion */,
|
|
content: reason || ""
|
|
});
|
|
e.tag(this);
|
|
await e.publish();
|
|
return e;
|
|
}
|
|
/**
|
|
* NIP-18 reposting event.
|
|
*
|
|
* @param publish Whether to publish the reposted event automatically
|
|
* @param signer The signer to use for signing the reposted event
|
|
* @returns The reposted event
|
|
*
|
|
* @function
|
|
*/
|
|
repost = repost.bind(this);
|
|
/**
|
|
* React to an existing event
|
|
*
|
|
* @param content The content of the reaction
|
|
*/
|
|
async react(content) {
|
|
if (!this.ndk)
|
|
throw new Error("No NDK instance found");
|
|
this.ndk.assertSigner();
|
|
const e = new NDKEvent(this.ndk, {
|
|
kind: 7 /* Reaction */,
|
|
content
|
|
});
|
|
e.tag(this);
|
|
await e.publish();
|
|
return e;
|
|
}
|
|
};
|
|
|
|
// src/events/kinds/NDKRelayList.ts
|
|
var READ_MARKER = "read";
|
|
var WRITE_MARKER = "write";
|
|
var NDKRelayList = class extends NDKEvent {
|
|
constructor(ndk, rawEvent) {
|
|
super(ndk, rawEvent);
|
|
this.kind ??= 10002 /* RelayList */;
|
|
}
|
|
static from(ndkEvent) {
|
|
return new NDKRelayList(ndkEvent.ndk, ndkEvent.rawEvent());
|
|
}
|
|
get readRelayUrls() {
|
|
return this.getMatchingTags("r").filter((tag) => !tag[2] || tag[2] && tag[2] === READ_MARKER).map((tag) => tag[1]);
|
|
}
|
|
set readRelayUrls(relays) {
|
|
for (const relay of relays) {
|
|
this.tags.push(["r", relay, READ_MARKER]);
|
|
}
|
|
}
|
|
get writeRelayUrls() {
|
|
return this.getMatchingTags("r").filter((tag) => !tag[2] || tag[2] && tag[2] === WRITE_MARKER).map((tag) => tag[1]);
|
|
}
|
|
set writeRelayUrls(relays) {
|
|
for (const relay of relays) {
|
|
this.tags.push(["r", relay, WRITE_MARKER]);
|
|
}
|
|
}
|
|
get bothRelayUrls() {
|
|
return this.getMatchingTags("r").filter((tag) => !tag[2]).map((tag) => tag[1]);
|
|
}
|
|
set bothRelayUrls(relays) {
|
|
for (const relay of relays) {
|
|
this.tags.push(["r", relay]);
|
|
}
|
|
}
|
|
get relays() {
|
|
return this.getMatchingTags("r").map((tag) => tag[1]);
|
|
}
|
|
};
|
|
|
|
// src/subscription/index.ts
|
|
var import_eventemitter35 = __toESM(require("eventemitter3"));
|
|
var NDKSubscriptionCacheUsage = /* @__PURE__ */ ((NDKSubscriptionCacheUsage2) => {
|
|
NDKSubscriptionCacheUsage2["ONLY_CACHE"] = "ONLY_CACHE";
|
|
NDKSubscriptionCacheUsage2["CACHE_FIRST"] = "CACHE_FIRST";
|
|
NDKSubscriptionCacheUsage2["PARALLEL"] = "PARALLEL";
|
|
NDKSubscriptionCacheUsage2["ONLY_RELAY"] = "ONLY_RELAY";
|
|
return NDKSubscriptionCacheUsage2;
|
|
})(NDKSubscriptionCacheUsage || {});
|
|
var defaultOpts = {
|
|
closeOnEose: false,
|
|
cacheUsage: "CACHE_FIRST" /* CACHE_FIRST */,
|
|
groupable: true,
|
|
groupableDelay: 100
|
|
};
|
|
var NDKSubscription = class extends import_eventemitter35.default {
|
|
subId;
|
|
filters;
|
|
opts;
|
|
pool;
|
|
/**
|
|
* Tracks the filters as they are executed on each relay
|
|
*/
|
|
relayFilters;
|
|
relaySet;
|
|
ndk;
|
|
debug;
|
|
eoseDebug;
|
|
/**
|
|
* Events that have been seen by the subscription, with the time they were first seen.
|
|
*/
|
|
eventFirstSeen = /* @__PURE__ */ new Map();
|
|
/**
|
|
* Relays that have sent an EOSE.
|
|
*/
|
|
eosesSeen = /* @__PURE__ */ new Set();
|
|
/**
|
|
* Events that have been seen by the subscription per relay.
|
|
*/
|
|
eventsPerRelay = /* @__PURE__ */ new Map();
|
|
/**
|
|
* The time the last event was received by the subscription.
|
|
* This is used to calculate when EOSE should be emitted.
|
|
*/
|
|
lastEventReceivedAt;
|
|
internalId;
|
|
constructor(ndk, filters, opts, relaySet, subId) {
|
|
super();
|
|
this.ndk = ndk;
|
|
this.pool = opts?.pool || ndk.pool;
|
|
this.opts = { ...defaultOpts, ...opts || {} };
|
|
this.filters = filters instanceof Array ? filters : [filters];
|
|
this.subId = subId || opts?.subId;
|
|
this.internalId = Math.random().toString(36).substring(7);
|
|
this.relaySet = relaySet;
|
|
this.debug = ndk.debug.extend(`subscription[${opts?.subId ?? this.internalId}]`);
|
|
5;
|
|
this.eoseDebug = this.debug.extend("eose");
|
|
if (!this.opts.closeOnEose) {
|
|
this.debug(
|
|
`Creating a permanent subscription`,
|
|
this.opts,
|
|
JSON.stringify(this.filters)
|
|
);
|
|
}
|
|
if (this.opts.cacheUsage === "ONLY_CACHE" /* ONLY_CACHE */ && !this.opts.closeOnEose) {
|
|
throw new Error("Cannot use cache-only options with a persistent subscription");
|
|
}
|
|
}
|
|
/**
|
|
* Provides access to the first filter of the subscription for
|
|
* backwards compatibility.
|
|
*/
|
|
get filter() {
|
|
return this.filters[0];
|
|
}
|
|
isGroupable() {
|
|
return this.opts?.groupable || false;
|
|
}
|
|
shouldQueryCache() {
|
|
return this.opts?.cacheUsage !== "ONLY_RELAY" /* ONLY_RELAY */;
|
|
}
|
|
shouldQueryRelays() {
|
|
return this.opts?.cacheUsage !== "ONLY_CACHE" /* ONLY_CACHE */;
|
|
}
|
|
shouldWaitForCache() {
|
|
return (
|
|
// Must want to close on EOSE; subscriptions
|
|
// that want to receive further updates must
|
|
// always hit the relay
|
|
this.opts.closeOnEose && // Cache adapter must claim to be fast
|
|
!!this.ndk.cacheAdapter?.locking && // If explicitly told to run in parallel, then
|
|
// we should not wait for the cache
|
|
this.opts.cacheUsage !== "PARALLEL" /* PARALLEL */
|
|
);
|
|
}
|
|
/**
|
|
* Start the subscription. This is the main method that should be called
|
|
* after creating a subscription.
|
|
*/
|
|
async start() {
|
|
let cachePromise;
|
|
if (this.shouldQueryCache()) {
|
|
cachePromise = this.startWithCache();
|
|
if (this.shouldWaitForCache()) {
|
|
await cachePromise;
|
|
if (queryFullyFilled(this)) {
|
|
this.emit("eose", this);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (this.shouldQueryRelays()) {
|
|
this.startWithRelays();
|
|
} else {
|
|
this.emit("eose", this);
|
|
}
|
|
return;
|
|
}
|
|
stop() {
|
|
this.emit("close", this);
|
|
}
|
|
/**
|
|
* @returns Whether the subscription has an authors filter.
|
|
*/
|
|
hasAuthorsFilter() {
|
|
return this.filters.some((f) => f.authors?.length);
|
|
}
|
|
async startWithCache() {
|
|
if (this.ndk.cacheAdapter?.query) {
|
|
const promise = this.ndk.cacheAdapter.query(this);
|
|
if (this.ndk.cacheAdapter.locking) {
|
|
await promise;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Send REQ to relays
|
|
*/
|
|
startWithRelays() {
|
|
if (!this.relaySet) {
|
|
this.relayFilters = calculateRelaySetsFromFilters(this.ndk, this.filters);
|
|
} else {
|
|
this.relayFilters = /* @__PURE__ */ new Map();
|
|
for (const relay of this.relaySet.relays) {
|
|
this.relayFilters.set(relay.url, this.filters);
|
|
}
|
|
}
|
|
for (const [relayUrl, filters] of this.relayFilters) {
|
|
const relay = this.pool.getRelay(relayUrl);
|
|
relay.subscribe(this, filters);
|
|
}
|
|
}
|
|
// EVENT handling
|
|
/**
|
|
* Called when an event is received from a relay or the cache
|
|
* @param event
|
|
* @param relay
|
|
* @param fromCache Whether the event was received from the cache
|
|
*/
|
|
eventReceived(event, relay, fromCache = false) {
|
|
if (relay)
|
|
event.relay = relay;
|
|
if (!relay)
|
|
relay = event.relay;
|
|
if (!fromCache && relay) {
|
|
let events = this.eventsPerRelay.get(relay);
|
|
if (!events) {
|
|
events = /* @__PURE__ */ new Set();
|
|
this.eventsPerRelay.set(relay, events);
|
|
}
|
|
events.add(event.id);
|
|
const eventAlreadySeen = this.eventFirstSeen.has(event.id);
|
|
if (eventAlreadySeen) {
|
|
const timeSinceFirstSeen = Date.now() - (this.eventFirstSeen.get(event.id) || 0);
|
|
relay.scoreSlowerEvent(timeSinceFirstSeen);
|
|
this.emit("event:dup", event, relay, timeSinceFirstSeen, this);
|
|
return;
|
|
}
|
|
if (this.ndk.cacheAdapter) {
|
|
this.ndk.cacheAdapter.setEvent(event, this.filters[0], relay);
|
|
}
|
|
this.eventFirstSeen.set(event.id, Date.now());
|
|
} else {
|
|
this.eventFirstSeen.set(event.id, 0);
|
|
}
|
|
if (!event.ndk)
|
|
event.ndk = this.ndk;
|
|
this.emit("event", event, relay, this);
|
|
this.lastEventReceivedAt = Date.now();
|
|
}
|
|
// EOSE handling
|
|
eoseTimeout;
|
|
eoseReceived(relay) {
|
|
this.eosesSeen.add(relay);
|
|
this.eoseDebug(`received from ${relay.url}`);
|
|
let lastEventSeen = this.lastEventReceivedAt ? Date.now() - this.lastEventReceivedAt : void 0;
|
|
const hasSeenAllEoses = this.eosesSeen.size === this.relayFilters?.size;
|
|
const queryFilled = queryFullyFilled(this);
|
|
if (queryFilled) {
|
|
this.emit("eose");
|
|
this.eoseDebug(`Query fully filled`);
|
|
if (this.opts?.closeOnEose) {
|
|
this.stop();
|
|
} else {
|
|
}
|
|
} else if (hasSeenAllEoses) {
|
|
this.emit("eose");
|
|
this.eoseDebug(`All EOSEs seen`);
|
|
if (this.opts?.closeOnEose) {
|
|
this.stop();
|
|
} else {
|
|
}
|
|
} else {
|
|
let timeToWaitForNextEose = 1e3;
|
|
const percentageOfRelaysThatHaveSentEose = this.eosesSeen.size / this.relayFilters.size;
|
|
if (this.eosesSeen.size >= 2 && percentageOfRelaysThatHaveSentEose >= 0.5) {
|
|
timeToWaitForNextEose = timeToWaitForNextEose * (1 - percentageOfRelaysThatHaveSentEose);
|
|
if (this.eoseTimeout) {
|
|
clearTimeout(this.eoseTimeout);
|
|
}
|
|
const sendEoseTimeout = () => {
|
|
lastEventSeen = this.lastEventReceivedAt ? Date.now() - this.lastEventReceivedAt : void 0;
|
|
if (lastEventSeen !== void 0 && lastEventSeen < 20) {
|
|
this.eoseTimeout = setTimeout(sendEoseTimeout, timeToWaitForNextEose);
|
|
} else {
|
|
this.emit("eose");
|
|
if (this.opts?.closeOnEose)
|
|
this.stop();
|
|
}
|
|
};
|
|
this.eoseTimeout = setTimeout(sendEoseTimeout, timeToWaitForNextEose);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/user/follows.ts
|
|
async function follows(opts, outbox) {
|
|
if (!this.ndk)
|
|
throw new Error("NDK not set");
|
|
const contactListEvent = await this.ndk.fetchEvent(
|
|
{
|
|
kinds: [3],
|
|
authors: [this.hexpubkey]
|
|
},
|
|
opts || { groupable: false }
|
|
);
|
|
if (contactListEvent) {
|
|
const pubkeys = /* @__PURE__ */ new Set();
|
|
contactListEvent.tags.forEach((tag) => {
|
|
if (tag[0] === "p") {
|
|
try {
|
|
pubkeys.add(tag[1]);
|
|
if (outbox) {
|
|
this.ndk?.outboxTracker?.trackUsers([tag[1]]);
|
|
}
|
|
} catch (e) {
|
|
}
|
|
}
|
|
});
|
|
return [...pubkeys].reduce((acc, hexpubkey) => {
|
|
const user = new NDKUser({ hexpubkey });
|
|
user.ndk = this.ndk;
|
|
acc.add(user);
|
|
return acc;
|
|
}, /* @__PURE__ */ new Set());
|
|
}
|
|
return /* @__PURE__ */ new Set();
|
|
}
|
|
|
|
// src/user/profile.ts
|
|
function profileFromEvent(event) {
|
|
const profile = {};
|
|
const payload = JSON.parse(event.content);
|
|
Object.keys(payload).forEach((key) => {
|
|
switch (key) {
|
|
case "name":
|
|
profile.name = payload.name;
|
|
break;
|
|
case "display_name":
|
|
profile.displayName = payload.display_name;
|
|
break;
|
|
case "image":
|
|
case "picture":
|
|
profile.image = payload.image || payload.picture;
|
|
break;
|
|
case "banner":
|
|
profile.banner = payload.banner;
|
|
break;
|
|
case "bio":
|
|
profile.bio = payload.bio;
|
|
break;
|
|
case "nip05":
|
|
profile.nip05 = payload.nip05;
|
|
break;
|
|
case "lud06":
|
|
profile.lud06 = payload.lud06;
|
|
break;
|
|
case "lud16":
|
|
profile.lud16 = payload.lud16;
|
|
break;
|
|
case "about":
|
|
profile.about = payload.about;
|
|
break;
|
|
case "zapService":
|
|
profile.zapService = payload.zapService;
|
|
break;
|
|
case "website":
|
|
profile.website = payload.website;
|
|
break;
|
|
default:
|
|
profile[key] = payload[key];
|
|
break;
|
|
}
|
|
});
|
|
return profile;
|
|
}
|
|
function serializeProfile(profile) {
|
|
const payload = {};
|
|
for (const [key, val] of Object.entries(profile)) {
|
|
switch (key) {
|
|
case "username":
|
|
case "name":
|
|
payload.name = val;
|
|
break;
|
|
case "displayName":
|
|
payload.display_name = val;
|
|
break;
|
|
case "image":
|
|
case "picture":
|
|
payload.picture = val;
|
|
break;
|
|
case "bio":
|
|
case "about":
|
|
payload.about = val;
|
|
break;
|
|
default:
|
|
payload[key] = val;
|
|
break;
|
|
}
|
|
}
|
|
return JSON.stringify(payload);
|
|
}
|
|
|
|
// src/user/index.ts
|
|
var NDKUser = class {
|
|
ndk;
|
|
profile;
|
|
_npub;
|
|
_pubkey;
|
|
relayUrls = [];
|
|
constructor(opts) {
|
|
if (opts.npub)
|
|
this._npub = opts.npub;
|
|
if (opts.hexpubkey)
|
|
this._pubkey = opts.hexpubkey;
|
|
if (opts.pubkey)
|
|
this._pubkey = opts.pubkey;
|
|
if (opts.relayUrls) {
|
|
this.relayUrls = opts.relayUrls;
|
|
}
|
|
}
|
|
get npub() {
|
|
if (!this._npub) {
|
|
if (!this._pubkey)
|
|
throw new Error("hexpubkey not set");
|
|
this._npub = import_nostr_tools8.nip19.npubEncode(this.hexpubkey);
|
|
}
|
|
return this._npub;
|
|
}
|
|
set npub(npub) {
|
|
this._npub = npub;
|
|
}
|
|
/**
|
|
* Get the user's hexpubkey
|
|
* @returns {Hexpubkey} The user's hexpubkey
|
|
*
|
|
* @deprecated Use `pubkey` instead
|
|
*/
|
|
get hexpubkey() {
|
|
return this.pubkey;
|
|
}
|
|
/**
|
|
* Set the user's hexpubkey
|
|
* @param pubkey {Hexpubkey} The user's hexpubkey
|
|
* @deprecated Use `pubkey` instead
|
|
*/
|
|
set hexpubkey(pubkey) {
|
|
this._pubkey = pubkey;
|
|
}
|
|
/**
|
|
* Get the user's pubkey
|
|
* @returns {string} The user's pubkey
|
|
*/
|
|
get pubkey() {
|
|
if (!this._pubkey) {
|
|
if (!this._npub)
|
|
throw new Error("npub not set");
|
|
this._pubkey = import_nostr_tools8.nip19.decode(this.npub).data;
|
|
}
|
|
return this._pubkey;
|
|
}
|
|
/**
|
|
* Set the user's pubkey
|
|
* @param pubkey {string} The user's pubkey
|
|
*/
|
|
set pubkey(pubkey) {
|
|
this._pubkey = pubkey;
|
|
}
|
|
/**
|
|
* Instantiate an NDKUser from a NIP-05 string
|
|
* @param nip05Id {string} The user's NIP-05
|
|
* @returns {NDKUser | undefined} An NDKUser if one is found for the given NIP-05, undefined otherwise.
|
|
*/
|
|
static async fromNip05(nip05Id) {
|
|
const profile = await import_nostr_tools8.nip05.queryProfile(nip05Id);
|
|
if (profile) {
|
|
return new NDKUser({
|
|
hexpubkey: profile.pubkey,
|
|
relayUrls: profile.relays
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Fetch a user's profile
|
|
* @param opts {NDKSubscriptionOptions} A set of NDKSubscriptionOptions
|
|
* @returns User Profile
|
|
*/
|
|
async fetchProfile(opts) {
|
|
if (!this.ndk)
|
|
throw new Error("NDK not set");
|
|
if (!this.profile)
|
|
this.profile = {};
|
|
let setMetadataEvents = null;
|
|
if (this.ndk.cacheAdapter && this.ndk.cacheAdapter.fetchProfile && opts?.cacheUsage !== "ONLY_RELAY" /* ONLY_RELAY */) {
|
|
const profile = await this.ndk.cacheAdapter.fetchProfile(this.hexpubkey);
|
|
if (profile) {
|
|
this.profile = profile;
|
|
return profile;
|
|
}
|
|
}
|
|
if (!opts && // if no options have been set
|
|
this.ndk.cacheAdapter && // and we have a cache
|
|
this.ndk.cacheAdapter.locking) {
|
|
setMetadataEvents = await this.ndk.fetchEvents(
|
|
{
|
|
kinds: [0],
|
|
authors: [this.hexpubkey]
|
|
},
|
|
{
|
|
cacheUsage: "ONLY_CACHE" /* ONLY_CACHE */,
|
|
closeOnEose: true,
|
|
groupable: false
|
|
}
|
|
);
|
|
opts = {
|
|
cacheUsage: "ONLY_RELAY" /* ONLY_RELAY */,
|
|
closeOnEose: true,
|
|
groupable: true,
|
|
groupableDelay: 250
|
|
};
|
|
}
|
|
if (!setMetadataEvents || setMetadataEvents.size === 0) {
|
|
setMetadataEvents = await this.ndk.fetchEvents(
|
|
{
|
|
kinds: [0],
|
|
authors: [this.hexpubkey]
|
|
},
|
|
opts
|
|
);
|
|
}
|
|
const sortedSetMetadataEvents = Array.from(setMetadataEvents).sort(
|
|
(a, b) => a.created_at - b.created_at
|
|
);
|
|
if (sortedSetMetadataEvents.length === 0)
|
|
return null;
|
|
this.profile = profileFromEvent(sortedSetMetadataEvents[0]);
|
|
if (this.profile && this.ndk.cacheAdapter && this.ndk.cacheAdapter.saveProfile) {
|
|
this.ndk.cacheAdapter.saveProfile(this.hexpubkey, this.profile);
|
|
}
|
|
return this.profile;
|
|
}
|
|
/**
|
|
* Returns a set of users that this user follows.
|
|
*/
|
|
follows = follows.bind(this);
|
|
/**
|
|
* Returns a set of relay list events for a user.
|
|
* @returns {Promise<Set<NDKEvent>>} A set of NDKEvents returned for the given user.
|
|
*/
|
|
async relayList() {
|
|
if (!this.ndk)
|
|
throw new Error("NDK not set");
|
|
const pool = this.ndk.outboxPool || this.ndk.pool;
|
|
const set = /* @__PURE__ */ new Set();
|
|
for (const relay of pool.relays.values())
|
|
set.add(relay);
|
|
const relaySet = new NDKRelaySet(set, this.ndk);
|
|
const event = await this.ndk.fetchEvent(
|
|
{
|
|
kinds: [10002],
|
|
authors: [this.hexpubkey]
|
|
},
|
|
{
|
|
closeOnEose: true,
|
|
pool,
|
|
groupable: true,
|
|
subId: `relay-list-${this.hexpubkey.slice(0, 6)}`
|
|
},
|
|
relaySet
|
|
);
|
|
if (event)
|
|
return NDKRelayList.from(event);
|
|
return await this.relayListFromKind3();
|
|
}
|
|
async relayListFromKind3() {
|
|
if (!this.ndk)
|
|
throw new Error("NDK not set");
|
|
const followList = await this.ndk.fetchEvent({
|
|
kinds: [3],
|
|
authors: [this.hexpubkey]
|
|
});
|
|
if (followList) {
|
|
try {
|
|
const content = JSON.parse(followList.content);
|
|
const relayList = new NDKRelayList(this.ndk);
|
|
const readRelays = /* @__PURE__ */ new Set();
|
|
const writeRelays = /* @__PURE__ */ new Set();
|
|
for (const [key, config] of Object.entries(content)) {
|
|
if (!config) {
|
|
readRelays.add(key);
|
|
writeRelays.add(key);
|
|
} else {
|
|
const relayConfig = config;
|
|
if (relayConfig.write)
|
|
writeRelays.add(key);
|
|
if (relayConfig.read)
|
|
readRelays.add(key);
|
|
}
|
|
}
|
|
relayList.readRelayUrls = Array.from(readRelays);
|
|
relayList.writeRelayUrls = Array.from(writeRelays);
|
|
return relayList;
|
|
} catch (e) {
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
/** @deprecated Use referenceTags instead. */
|
|
/**
|
|
* Get the tag that can be used to reference this user in an event
|
|
* @returns {NDKTag} an NDKTag
|
|
*/
|
|
tagReference() {
|
|
return ["p", this.hexpubkey];
|
|
}
|
|
/**
|
|
* Get the tags that can be used to reference this user in an event
|
|
* @returns {NDKTag[]} an array of NDKTag
|
|
*/
|
|
referenceTags() {
|
|
return [["p", this.hexpubkey]];
|
|
}
|
|
/**
|
|
* Publishes the current profile.
|
|
*/
|
|
async publish() {
|
|
if (!this.ndk)
|
|
throw new Error("No NDK instance found");
|
|
if (!this.profile)
|
|
throw new Error("No profile available");
|
|
this.ndk.assertSigner();
|
|
const event = new NDKEvent(this.ndk, {
|
|
kind: 0,
|
|
content: serializeProfile(this.profile)
|
|
});
|
|
await event.publish();
|
|
}
|
|
/**
|
|
* Add a follow to this user's contact list
|
|
*
|
|
* @param newFollow {NDKUser} The user to follow
|
|
* @param currentFollowList {Set<NDKUser>} The current follow list
|
|
* @returns {Promise<boolean>} True if the follow was added, false if the follow already exists
|
|
*/
|
|
async follow(newFollow, currentFollowList) {
|
|
if (!this.ndk)
|
|
throw new Error("No NDK instance found");
|
|
this.ndk.assertSigner();
|
|
if (!currentFollowList) {
|
|
currentFollowList = await this.follows();
|
|
}
|
|
if (currentFollowList.has(newFollow)) {
|
|
return false;
|
|
}
|
|
currentFollowList.add(newFollow);
|
|
const event = new NDKEvent(this.ndk, {
|
|
kind: 3 /* Contacts */
|
|
});
|
|
for (const follow of currentFollowList) {
|
|
event.tag(follow);
|
|
}
|
|
await event.publish();
|
|
return true;
|
|
}
|
|
/**
|
|
* Validate a user's NIP-05 identifier (usually fetched from their kind:0 profile data)
|
|
*
|
|
* @param nip05Id The NIP-05 string to validate
|
|
* @returns {Promise<boolean | null>} True if the NIP-05 is found and matches this user's hexpubkey,
|
|
* False if the NIP-05 is found but doesn't match this user's hexpubkey,
|
|
* null if the NIP-05 isn't found on the domain or we're unable to verify (because of network issues, etc.)
|
|
*/
|
|
async validateNip05(nip05Id) {
|
|
if (!this.ndk)
|
|
throw new Error("No NDK instance found");
|
|
const profilePointer = await import_nostr_tools8.nip05.queryProfile(nip05Id);
|
|
if (profilePointer === null)
|
|
return null;
|
|
return profilePointer.pubkey === this.hexpubkey;
|
|
}
|
|
/**
|
|
* Zap a user
|
|
*
|
|
* @param amount The amount to zap in millisatoshis
|
|
* @param comment A comment to add to the zap request
|
|
* @param extraTags Extra tags to add to the zap request
|
|
* @param signer The signer to use (will default to the NDK instance's signer)
|
|
*/
|
|
async zap(amount, comment, extraTags, signer) {
|
|
if (!this.ndk)
|
|
throw new Error("No NDK instance found");
|
|
if (!signer) {
|
|
this.ndk.assertSigner();
|
|
}
|
|
const zap = new Zap({
|
|
ndk: this.ndk,
|
|
zappedUser: this
|
|
});
|
|
const relays = Array.from(this.ndk.pool.relays.keys());
|
|
const paymentRequest = await zap.createZapRequest(
|
|
amount,
|
|
comment,
|
|
extraTags,
|
|
relays,
|
|
signer
|
|
);
|
|
return paymentRequest;
|
|
}
|
|
};
|
|
|
|
// src/events/kinds/article.ts
|
|
var NDKArticle = class extends NDKEvent {
|
|
constructor(ndk, rawEvent) {
|
|
super(ndk, rawEvent);
|
|
this.kind = 30023 /* Article */;
|
|
}
|
|
/**
|
|
* Creates a NDKArticle from an existing NDKEvent.
|
|
*
|
|
* @param event NDKEvent to create the NDKArticle from.
|
|
* @returns NDKArticle
|
|
*/
|
|
static from(event) {
|
|
return new NDKArticle(event.ndk, event.rawEvent());
|
|
}
|
|
/**
|
|
* Getter for the article title.
|
|
*
|
|
* @returns {string | undefined} - The article title if available, otherwise undefined.
|
|
*/
|
|
get title() {
|
|
return this.tagValue("title");
|
|
}
|
|
/**
|
|
* Setter for the article title.
|
|
*
|
|
* @param {string | undefined} title - The title to set for the article.
|
|
*/
|
|
set title(title) {
|
|
this.removeTag("title");
|
|
if (title)
|
|
this.tags.push(["title", title]);
|
|
}
|
|
/**
|
|
* Getter for the article image.
|
|
*
|
|
* @returns {string | undefined} - The article image if available, otherwise undefined.
|
|
*/
|
|
get image() {
|
|
return this.tagValue("image");
|
|
}
|
|
/**
|
|
* Setter for the article image.
|
|
*
|
|
* @param {string | undefined} image - The image to set for the article.
|
|
*/
|
|
set image(image) {
|
|
this.removeTag("image");
|
|
if (image)
|
|
this.tags.push(["image", image]);
|
|
}
|
|
get summary() {
|
|
return this.tagValue("summary");
|
|
}
|
|
/**
|
|
* Getter for the article's publication timestamp.
|
|
*
|
|
* @returns {number | undefined} - The Unix timestamp of when the article was published or undefined.
|
|
*/
|
|
get published_at() {
|
|
const tag = this.tagValue("published_at");
|
|
if (tag) {
|
|
return parseInt(tag);
|
|
}
|
|
return void 0;
|
|
}
|
|
/**
|
|
* Setter for the article's publication timestamp.
|
|
*
|
|
* @param {number | undefined} timestamp - The Unix timestamp to set for the article's publication date.
|
|
*/
|
|
set published_at(timestamp) {
|
|
this.removeTag("published_at");
|
|
if (timestamp !== void 0) {
|
|
this.tags.push(["published_at", timestamp.toString()]);
|
|
}
|
|
}
|
|
/**
|
|
* Generates content tags for the article.
|
|
*
|
|
* This method first checks and sets the publication date if not available,
|
|
* and then generates content tags based on the base NDKEvent class.
|
|
*
|
|
* @returns {ContentTag} - The generated content tags.
|
|
*/
|
|
async generateTags() {
|
|
super.generateTags();
|
|
if (!this.published_at) {
|
|
this.published_at = this.created_at;
|
|
}
|
|
return super.generateTags();
|
|
}
|
|
/**
|
|
* Getter for the article's URL.
|
|
*
|
|
* @returns {string | undefined} - The article's URL if available, otherwise undefined.
|
|
*/
|
|
get url() {
|
|
return this.tagValue("url");
|
|
}
|
|
/**
|
|
* Setter for the article's URL.
|
|
*
|
|
* @param {string | undefined} url - The URL to set for the article.
|
|
*/
|
|
set url(url) {
|
|
if (url) {
|
|
this.tags.push(["url", url]);
|
|
} else {
|
|
this.removeTag("url");
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/events/kinds/highlight.ts
|
|
var import_nostr_tools9 = require("nostr-tools");
|
|
var NDKHighlight = class extends NDKEvent {
|
|
_article;
|
|
constructor(ndk, rawEvent) {
|
|
super(ndk, rawEvent);
|
|
this.kind ??= 9802 /* Highlight */;
|
|
}
|
|
static from(event) {
|
|
return new NDKHighlight(event.ndk, event.rawEvent());
|
|
}
|
|
get url() {
|
|
return this.tagValue("r");
|
|
}
|
|
/**
|
|
* Context tag.
|
|
*/
|
|
set context(context) {
|
|
if (context === void 0) {
|
|
this.tags = this.tags.filter(([tag, value]) => tag !== "context");
|
|
} else {
|
|
this.tags = this.tags.filter(([tag, value]) => tag !== "context");
|
|
this.tags.push(["context", context]);
|
|
}
|
|
}
|
|
get context() {
|
|
return this.tags.find(([tag, value]) => tag === "context")?.[1] ?? void 0;
|
|
}
|
|
/**
|
|
* Will return the article URL or NDKEvent if they have already been
|
|
* set (it won't attempt to load remote events)
|
|
*/
|
|
get article() {
|
|
return this._article;
|
|
}
|
|
/**
|
|
* Article the highlight is coming from.
|
|
*
|
|
* @param article Article URL or NDKEvent.
|
|
*/
|
|
set article(article) {
|
|
this._article = article;
|
|
if (typeof article === "string") {
|
|
this.tags.push(["r", article]);
|
|
} else {
|
|
this.tag(article);
|
|
}
|
|
}
|
|
getArticleTag() {
|
|
return this.getMatchingTags("a")[0] || this.getMatchingTags("e")[0] || this.getMatchingTags("r")[0];
|
|
}
|
|
async getArticle() {
|
|
if (this._article !== void 0)
|
|
return this._article;
|
|
let taggedBech32;
|
|
const articleTag = this.getArticleTag();
|
|
if (!articleTag)
|
|
return void 0;
|
|
switch (articleTag[0]) {
|
|
case "a":
|
|
const [kind, pubkey, identifier] = articleTag[1].split(":");
|
|
taggedBech32 = import_nostr_tools9.nip19.naddrEncode({ kind: parseInt(kind), pubkey, identifier });
|
|
break;
|
|
case "e":
|
|
taggedBech32 = import_nostr_tools9.nip19.noteEncode(articleTag[1]);
|
|
break;
|
|
case "r":
|
|
this._article = articleTag[1];
|
|
break;
|
|
}
|
|
if (taggedBech32) {
|
|
let a = await this.ndk?.fetchEvent(taggedBech32);
|
|
if (a) {
|
|
if (a.kind === 30023 /* Article */) {
|
|
a = NDKArticle.from(a);
|
|
}
|
|
this._article = a;
|
|
}
|
|
}
|
|
return this._article;
|
|
}
|
|
};
|
|
|
|
// src/events/kinds/dvm/NDKDVMJobFeedback.ts
|
|
var NDKDvmJobFeedbackStatus = /* @__PURE__ */ ((NDKDvmJobFeedbackStatus2) => {
|
|
NDKDvmJobFeedbackStatus2["Processing"] = "processing";
|
|
NDKDvmJobFeedbackStatus2["Success"] = "success";
|
|
NDKDvmJobFeedbackStatus2["Scheduled"] = "scheduled";
|
|
NDKDvmJobFeedbackStatus2["PayReq"] = "payment_required";
|
|
return NDKDvmJobFeedbackStatus2;
|
|
})(NDKDvmJobFeedbackStatus || {});
|
|
var NDKDVMJobFeedback = class extends NDKEvent {
|
|
constructor(ndk, event) {
|
|
super(ndk, event);
|
|
this.kind ??= 7e3 /* DVMJobFeedback */;
|
|
}
|
|
static from(event) {
|
|
return new NDKDVMJobFeedback(event.ndk, event.rawEvent());
|
|
}
|
|
get status() {
|
|
return this.tagValue("status");
|
|
}
|
|
set status(status) {
|
|
this.removeTag("status");
|
|
if (status !== void 0) {
|
|
this.tags.push(["status", status]);
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/events/kinds/dvm/NDKDVMJobResult.ts
|
|
var NDKDVMJobResult = class extends NDKEvent {
|
|
constructor(ndk, event) {
|
|
super(ndk, event);
|
|
}
|
|
static from(event) {
|
|
return new NDKDVMJobResult(event.ndk, event.rawEvent());
|
|
}
|
|
setAmount(msat, invoice) {
|
|
this.removeTag("amount");
|
|
const tag = ["amount", msat.toString()];
|
|
if (invoice)
|
|
tag.push(invoice);
|
|
this.tags.push(tag);
|
|
}
|
|
set result(result) {
|
|
if (result === void 0) {
|
|
this.content = "";
|
|
} else {
|
|
this.content = result;
|
|
}
|
|
}
|
|
get result() {
|
|
if (this.content === "") {
|
|
return void 0;
|
|
}
|
|
return this.content;
|
|
}
|
|
set status(status) {
|
|
this.removeTag("status");
|
|
if (status !== void 0) {
|
|
this.tags.push(["status", status]);
|
|
}
|
|
}
|
|
get status() {
|
|
return this.tagValue("status");
|
|
}
|
|
get jobRequestId() {
|
|
for (const eTag of this.getMatchingTags("e")) {
|
|
if (eTag[2] === "job")
|
|
return eTag[1];
|
|
}
|
|
if (this.jobRequest)
|
|
return this.jobRequest.id;
|
|
return this.tagValue("e");
|
|
}
|
|
set jobRequest(event) {
|
|
this.removeTag("request");
|
|
if (event) {
|
|
this.kind = event.kind + 1e3;
|
|
this.tags.push(["request", JSON.stringify(event.rawEvent())]);
|
|
this.tag(event);
|
|
}
|
|
}
|
|
get jobRequest() {
|
|
const tag = this.tagValue("request");
|
|
if (tag === void 0) {
|
|
return void 0;
|
|
}
|
|
return new NDKEvent(this.ndk, JSON.parse(tag));
|
|
}
|
|
};
|
|
|
|
// src/events/kinds/dvm/NDKDVMRequest.ts
|
|
var NDKDVMRequest = class extends NDKEvent {
|
|
constructor(ndk, event) {
|
|
super(ndk, event);
|
|
if (ndk) {
|
|
this.tags.push(["relays", ...ndk.pool.urls()]);
|
|
}
|
|
}
|
|
static from(event) {
|
|
return new NDKDVMRequest(event.ndk, event.rawEvent());
|
|
}
|
|
/**
|
|
* Create a new job feedback for this request
|
|
*/
|
|
createFeedback(status) {
|
|
const feedback = new NDKDVMJobFeedback(this.ndk);
|
|
feedback.status = status;
|
|
feedback.tag(this, "job");
|
|
return feedback;
|
|
}
|
|
/**
|
|
* Create a new result event for this request
|
|
*/
|
|
createResult(data) {
|
|
const event = new NDKDVMJobResult(this.ndk, data);
|
|
event.jobRequest = this;
|
|
return event;
|
|
}
|
|
set bid(msatAmount) {
|
|
if (msatAmount === void 0) {
|
|
this.removeTag("bid");
|
|
} else {
|
|
this.tags.push(["bid", msatAmount.toString()]);
|
|
}
|
|
}
|
|
get bid() {
|
|
const v = this.tagValue("bid");
|
|
if (v === void 0)
|
|
return void 0;
|
|
return parseInt(v);
|
|
}
|
|
/**
|
|
* Adds a new input to the job
|
|
* @param args The arguments to the input
|
|
*/
|
|
addInput(...args) {
|
|
this.tags.push(["i", ...args]);
|
|
}
|
|
/**
|
|
* Adds a new parameter to the job
|
|
*/
|
|
addParam(...args) {
|
|
this.tags.push(["param", ...args]);
|
|
}
|
|
set output(output) {
|
|
if (output === void 0) {
|
|
this.removeTag("output");
|
|
} else {
|
|
if (typeof output === "string")
|
|
output = [output];
|
|
this.tags.push(["output", ...output]);
|
|
}
|
|
}
|
|
get output() {
|
|
const outputTag = this.getMatchingTags("output")[0];
|
|
return outputTag ? outputTag.slice(1) : void 0;
|
|
}
|
|
get params() {
|
|
const paramTags = this.getMatchingTags("param");
|
|
return paramTags.map((t) => t.slice(1));
|
|
}
|
|
getParam(name) {
|
|
const paramTag = this.getMatchingTags("param").find((t) => t[1] === name);
|
|
return paramTag ? paramTag[2] : void 0;
|
|
}
|
|
};
|
|
|
|
// src/events/kinds/dvm/NDKTranscriptionDVM.ts
|
|
var NDKTranscriptionDVM = class extends NDKDVMRequest {
|
|
constructor(ndk, event) {
|
|
super(ndk, event);
|
|
this.kind = 5e3 /* DVMReqTextExtraction */;
|
|
}
|
|
static from(event) {
|
|
return new NDKTranscriptionDVM(event.ndk, event.rawEvent());
|
|
}
|
|
/**
|
|
* Returns the original source of the transcription
|
|
*/
|
|
get url() {
|
|
const inputTags = this.getMatchingTags("i");
|
|
if (inputTags.length !== 1) {
|
|
return void 0;
|
|
}
|
|
return inputTags[0][1];
|
|
}
|
|
/**
|
|
* Getter for the title tag
|
|
*/
|
|
get title() {
|
|
return this.tagValue("title");
|
|
}
|
|
/**
|
|
* Setter for the title tag
|
|
*/
|
|
set title(value) {
|
|
this.removeTag("title");
|
|
if (value) {
|
|
this.tags.push(["title", value]);
|
|
}
|
|
}
|
|
/**
|
|
* Getter for the image tag
|
|
*/
|
|
get image() {
|
|
return this.tagValue("image");
|
|
}
|
|
/**
|
|
* Setter for the image tag
|
|
*/
|
|
set image(value) {
|
|
this.removeTag("image");
|
|
if (value) {
|
|
this.tags.push(["image", value]);
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/events/kinds/lists/index.ts
|
|
var NDKList = class extends NDKEvent {
|
|
_encryptedTags;
|
|
/**
|
|
* Stores the number of bytes the content was before decryption
|
|
* to expire the cache when the content changes.
|
|
*/
|
|
encryptedTagsLength;
|
|
constructor(ndk, rawEvent) {
|
|
super(ndk, rawEvent);
|
|
this.kind ??= 30001 /* CategorizedBookmarkList */;
|
|
}
|
|
/**
|
|
* Wrap a NDKEvent into a NDKList
|
|
*/
|
|
static from(ndkEvent) {
|
|
return new NDKList(ndkEvent.ndk, ndkEvent.rawEvent());
|
|
}
|
|
/**
|
|
* Returns the title of the list. Falls back on fetching the name tag value.
|
|
*/
|
|
get title() {
|
|
const titleTag = this.tagValue("title") || this.tagValue("name");
|
|
if (this.kind === 3 /* Contacts */ && !titleTag) {
|
|
return "Contacts";
|
|
} else if (this.kind === 1e4 /* MuteList */ && !titleTag) {
|
|
return "Mute";
|
|
} else if (this.kind === 10001 /* PinList */ && !titleTag) {
|
|
return "Pin";
|
|
} else if (this.kind === 10002 /* RelayList */ && !titleTag) {
|
|
return "Relay Metadata";
|
|
} else {
|
|
return titleTag ?? this.tagValue("d");
|
|
}
|
|
}
|
|
/**
|
|
* Sets the title of the list.
|
|
*/
|
|
set title(title) {
|
|
this.removeTag("title");
|
|
this.removeTag("name");
|
|
if (title) {
|
|
this.tags.push(["title", title]);
|
|
} else {
|
|
throw new Error("Title cannot be empty");
|
|
}
|
|
}
|
|
/**
|
|
* Returns the name of the list.
|
|
* @deprecated Please use "title" instead.
|
|
*/
|
|
get name() {
|
|
const nameTag = this.tagValue("name");
|
|
if (this.kind === 3 /* Contacts */ && !nameTag) {
|
|
return "Contacts";
|
|
} else if (this.kind === 1e4 /* MuteList */ && !nameTag) {
|
|
return "Mute";
|
|
} else if (this.kind === 10001 /* PinList */ && !nameTag) {
|
|
return "Pin";
|
|
} else if (this.kind === 10002 /* RelayList */ && !nameTag) {
|
|
return "Relay Metadata";
|
|
} else {
|
|
return nameTag ?? this.tagValue("d");
|
|
}
|
|
}
|
|
/**
|
|
* Sets the name of the list.
|
|
* @deprecated Please use "title" instead. This method will use the `title` tag instead.
|
|
*/
|
|
set name(name) {
|
|
this.removeTag("name");
|
|
if (name) {
|
|
this.tags.push(["title", name]);
|
|
} else {
|
|
throw new Error("Name cannot be empty");
|
|
}
|
|
}
|
|
/**
|
|
* Returns the description of the list.
|
|
*/
|
|
get description() {
|
|
return this.tagValue("description");
|
|
}
|
|
/**
|
|
* Sets the description of the list.
|
|
*/
|
|
set description(name) {
|
|
if (name) {
|
|
this.tags.push(["description", name]);
|
|
} else {
|
|
this.removeTag("description");
|
|
}
|
|
}
|
|
isEncryptedTagsCacheValid() {
|
|
return !!(this._encryptedTags && this.encryptedTagsLength === this.content.length);
|
|
}
|
|
/**
|
|
* Returns the decrypted content of the list.
|
|
*/
|
|
async encryptedTags(useCache = true) {
|
|
if (useCache && this.isEncryptedTagsCacheValid())
|
|
return this._encryptedTags;
|
|
if (!this.ndk)
|
|
throw new Error("NDK instance not set");
|
|
if (!this.ndk.signer)
|
|
throw new Error("NDK signer not set");
|
|
const user = await this.ndk.signer.user();
|
|
try {
|
|
if (this.content.length > 0) {
|
|
try {
|
|
const decryptedContent = await this.ndk.signer.decrypt(user, this.content);
|
|
const a = JSON.parse(decryptedContent);
|
|
if (a && a[0]) {
|
|
this.encryptedTagsLength = this.content.length;
|
|
return this._encryptedTags = a;
|
|
}
|
|
this.encryptedTagsLength = this.content.length;
|
|
return this._encryptedTags = [];
|
|
} catch (e) {
|
|
console.log(`error decrypting ${this.content}`);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
}
|
|
return [];
|
|
}
|
|
/**
|
|
* This method can be overriden to validate that a tag is valid for this list.
|
|
*
|
|
* (i.e. the NDKPersonList can validate that items are NDKUser instances)
|
|
*/
|
|
validateTag(tagValue) {
|
|
return true;
|
|
}
|
|
/**
|
|
* Returns the unecrypted items in this list.
|
|
*/
|
|
get items() {
|
|
return this.tags.filter((t) => {
|
|
return !["d", "name", "description"].includes(t[0]);
|
|
});
|
|
}
|
|
/**
|
|
* Adds a new item to the list.
|
|
* @param relay Relay to add
|
|
* @param mark Optional mark to add to the item
|
|
* @param encrypted Whether to encrypt the item
|
|
*/
|
|
async addItem(item, mark = void 0, encrypted = false) {
|
|
if (!this.ndk)
|
|
throw new Error("NDK instance not set");
|
|
if (!this.ndk.signer)
|
|
throw new Error("NDK signer not set");
|
|
let tags;
|
|
if (item instanceof NDKEvent) {
|
|
tags = item.referenceTags();
|
|
} else if (item instanceof NDKUser) {
|
|
tags = item.referenceTags();
|
|
} else if (item instanceof NDKRelay) {
|
|
tags = item.referenceTags();
|
|
} else if (Array.isArray(item)) {
|
|
tags = [item];
|
|
} else {
|
|
throw new Error("Invalid object type");
|
|
}
|
|
if (mark)
|
|
tags[0].push(mark);
|
|
if (encrypted) {
|
|
const user = await this.ndk.signer.user();
|
|
const currentList = await this.encryptedTags();
|
|
currentList.push(...tags);
|
|
this._encryptedTags = currentList;
|
|
this.encryptedTagsLength = this.content.length;
|
|
this.content = JSON.stringify(currentList);
|
|
await this.encrypt(user);
|
|
} else {
|
|
this.tags.push(...tags);
|
|
}
|
|
this.created_at = Math.floor(Date.now() / 1e3);
|
|
this.emit("change");
|
|
}
|
|
/**
|
|
* Removes an item from the list.
|
|
*
|
|
* @param index The index of the item to remove.
|
|
* @param encrypted Whether to remove from the encrypted list or not.
|
|
*/
|
|
async removeItem(index, encrypted) {
|
|
if (!this.ndk)
|
|
throw new Error("NDK instance not set");
|
|
if (!this.ndk.signer)
|
|
throw new Error("NDK signer not set");
|
|
if (encrypted) {
|
|
const user = await this.ndk.signer.user();
|
|
const currentList = await this.encryptedTags();
|
|
currentList.splice(index, 1);
|
|
this._encryptedTags = currentList;
|
|
this.encryptedTagsLength = this.content.length;
|
|
this.content = JSON.stringify(currentList);
|
|
await this.encrypt(user);
|
|
} else {
|
|
this.tags.splice(index, 1);
|
|
}
|
|
this.created_at = Math.floor(Date.now() / 1e3);
|
|
this.emit("change");
|
|
return this;
|
|
}
|
|
};
|
|
var lists_default = NDKList;
|
|
|
|
// src/events/kinds/repost.ts
|
|
var NDKRepost = class extends NDKEvent {
|
|
_repostedEvents;
|
|
constructor(ndk, rawEvent) {
|
|
super(ndk, rawEvent);
|
|
}
|
|
static from(event) {
|
|
return new NDKRepost(event.ndk, event.rawEvent());
|
|
}
|
|
/**
|
|
* Returns all reposted events by the current event.
|
|
*
|
|
* @param klass Optional class to convert the events to.
|
|
* @returns
|
|
*/
|
|
async repostedEvents(klass, opts) {
|
|
const items = [];
|
|
if (!this.ndk)
|
|
throw new Error("NDK instance not set");
|
|
if (this._repostedEvents !== void 0)
|
|
return this._repostedEvents;
|
|
for (const eventId of this.repostedEventIds()) {
|
|
const filter = filterForId(eventId);
|
|
const event = await this.ndk.fetchEvent(filter, opts);
|
|
if (event) {
|
|
items.push(klass ? klass.from(event) : event);
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
/**
|
|
* Returns the reposted event IDs.
|
|
*/
|
|
repostedEventIds() {
|
|
return this.tags.filter((t) => t[0] === "e" || t[0] === "a").map((t) => t[1]);
|
|
}
|
|
};
|
|
function filterForId(id) {
|
|
if (id.match(/:/)) {
|
|
const [kind, pubkey, identifier] = id.split(":");
|
|
return {
|
|
kinds: [parseInt(kind)],
|
|
authors: [pubkey],
|
|
"#d": [identifier]
|
|
};
|
|
} else {
|
|
return { ids: [id] };
|
|
}
|
|
}
|
|
|
|
// src/events/kinds/nip89/NDKAppHandler.ts
|
|
var NDKAppHandlerEvent = class extends NDKEvent {
|
|
profile;
|
|
constructor(ndk, rawEvent) {
|
|
super(ndk, rawEvent);
|
|
this.kind ??= 31990 /* AppHandler */;
|
|
}
|
|
static from(ndkEvent) {
|
|
return new NDKAppHandlerEvent(ndkEvent.ndk, ndkEvent.rawEvent());
|
|
}
|
|
/**
|
|
* Fetches app handler information
|
|
* If no app information is available on the kind:31990,
|
|
* we fetch the event's author's profile and return that instead.
|
|
*/
|
|
async fetchProfile() {
|
|
if (this.profile === void 0 && this.content.length > 0) {
|
|
try {
|
|
const profile = JSON.parse(this.content);
|
|
if (profile && profile.name) {
|
|
return profile;
|
|
} else {
|
|
this.profile = null;
|
|
}
|
|
} catch (e) {
|
|
this.profile = null;
|
|
}
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const author = this.author;
|
|
author.fetchProfile().then(() => {
|
|
resolve(author.profile);
|
|
}).catch(reject);
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/signers/nip07/index.ts
|
|
var import_debug2 = __toESM(require("debug"));
|
|
var NDKNip07Signer = class {
|
|
_userPromise;
|
|
nip04Queue = [];
|
|
nip04Processing = false;
|
|
debug;
|
|
waitTimeout;
|
|
/**
|
|
* @param waitTimeout - The timeout in milliseconds to wait for the NIP-07 to become available
|
|
*/
|
|
constructor(waitTimeout = 1e3) {
|
|
this.debug = (0, import_debug2.default)("ndk:nip07");
|
|
this.waitTimeout = waitTimeout;
|
|
}
|
|
async blockUntilReady() {
|
|
await this.waitForExtension();
|
|
const pubkey = await window.nostr.getPublicKey();
|
|
if (!pubkey) {
|
|
throw new Error("User rejected access");
|
|
}
|
|
return new NDKUser({ hexpubkey: pubkey });
|
|
}
|
|
/**
|
|
* Getter for the user property.
|
|
* @returns The NDKUser instance.
|
|
*/
|
|
async user() {
|
|
if (!this._userPromise) {
|
|
this._userPromise = this.blockUntilReady();
|
|
}
|
|
return this._userPromise;
|
|
}
|
|
/**
|
|
* Signs the given Nostr event.
|
|
* @param event - The Nostr event to be signed.
|
|
* @returns The signature of the signed event.
|
|
* @throws Error if the NIP-07 is not available on the window object.
|
|
*/
|
|
async sign(event) {
|
|
await this.waitForExtension();
|
|
const signedEvent = await window.nostr.signEvent(event);
|
|
return signedEvent.sig;
|
|
}
|
|
async encrypt(recipient, value) {
|
|
await this.waitForExtension();
|
|
const recipientHexPubKey = recipient.hexpubkey;
|
|
return this.queueNip04("encrypt", recipientHexPubKey, value);
|
|
}
|
|
async decrypt(sender, value) {
|
|
await this.waitForExtension();
|
|
const senderHexPubKey = sender.hexpubkey;
|
|
return this.queueNip04("decrypt", senderHexPubKey, value);
|
|
}
|
|
async queueNip04(type, counterpartyHexpubkey, value) {
|
|
return new Promise((resolve, reject) => {
|
|
this.nip04Queue.push({
|
|
type,
|
|
counterpartyHexpubkey,
|
|
value,
|
|
resolve,
|
|
reject
|
|
});
|
|
if (!this.nip04Processing) {
|
|
this.processNip04Queue();
|
|
}
|
|
});
|
|
}
|
|
async processNip04Queue(item, retries = 0) {
|
|
if (!item && this.nip04Queue.length === 0) {
|
|
this.nip04Processing = false;
|
|
return;
|
|
}
|
|
this.nip04Processing = true;
|
|
const { type, counterpartyHexpubkey, value, resolve, reject } = item || this.nip04Queue.shift();
|
|
this.debug("Processing encryption queue item", {
|
|
type,
|
|
counterpartyHexpubkey,
|
|
value
|
|
});
|
|
try {
|
|
let result;
|
|
if (type === "encrypt") {
|
|
result = await window.nostr.nip04.encrypt(counterpartyHexpubkey, value);
|
|
} else {
|
|
result = await window.nostr.nip04.decrypt(counterpartyHexpubkey, value);
|
|
}
|
|
resolve(result);
|
|
} catch (error) {
|
|
if (error.message && error.message.includes("call already executing")) {
|
|
if (retries < 5) {
|
|
this.debug("Retrying encryption queue item", {
|
|
type,
|
|
counterpartyHexpubkey,
|
|
value,
|
|
retries
|
|
});
|
|
setTimeout(() => {
|
|
this.processNip04Queue(item, retries + 1);
|
|
}, 50 * retries);
|
|
return;
|
|
}
|
|
}
|
|
reject(error);
|
|
}
|
|
this.processNip04Queue();
|
|
}
|
|
waitForExtension() {
|
|
return new Promise((resolve, reject) => {
|
|
if (window.nostr) {
|
|
resolve();
|
|
return;
|
|
}
|
|
let timerId;
|
|
const intervalId = setInterval(() => {
|
|
if (window.nostr) {
|
|
clearTimeout(timerId);
|
|
clearInterval(intervalId);
|
|
resolve();
|
|
}
|
|
}, 100);
|
|
timerId = setTimeout(() => {
|
|
clearInterval(intervalId);
|
|
reject(new Error("NIP-07 extension not available"));
|
|
}, this.waitTimeout);
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/signers/nip46/backend/index.ts
|
|
var import_nostr_tools11 = require("nostr-tools");
|
|
|
|
// src/signers/private-key/index.ts
|
|
var import_nostr_tools10 = require("nostr-tools");
|
|
var NDKPrivateKeySigner = class {
|
|
_user;
|
|
privateKey;
|
|
constructor(privateKey) {
|
|
if (privateKey) {
|
|
this.privateKey = privateKey;
|
|
this._user = new NDKUser({
|
|
hexpubkey: (0, import_nostr_tools10.getPublicKey)(this.privateKey)
|
|
});
|
|
}
|
|
}
|
|
static generate() {
|
|
const privateKey = (0, import_nostr_tools10.generatePrivateKey)();
|
|
return new NDKPrivateKeySigner(privateKey);
|
|
}
|
|
async blockUntilReady() {
|
|
if (!this._user) {
|
|
throw new Error("NDKUser not initialized");
|
|
}
|
|
return this._user;
|
|
}
|
|
async user() {
|
|
await this.blockUntilReady();
|
|
return this._user;
|
|
}
|
|
async sign(event) {
|
|
if (!this.privateKey) {
|
|
throw Error("Attempted to sign without a private key");
|
|
}
|
|
return (0, import_nostr_tools10.getSignature)(event, this.privateKey);
|
|
}
|
|
async encrypt(recipient, value) {
|
|
if (!this.privateKey) {
|
|
throw Error("Attempted to encrypt without a private key");
|
|
}
|
|
const recipientHexPubKey = recipient.hexpubkey;
|
|
return await import_nostr_tools10.nip04.encrypt(this.privateKey, recipientHexPubKey, value);
|
|
}
|
|
async decrypt(sender, value) {
|
|
if (!this.privateKey) {
|
|
throw Error("Attempted to decrypt without a private key");
|
|
}
|
|
const senderHexPubKey = sender.hexpubkey;
|
|
return await import_nostr_tools10.nip04.decrypt(this.privateKey, senderHexPubKey, value);
|
|
}
|
|
};
|
|
|
|
// src/signers/nip46/rpc.ts
|
|
var import_eventemitter36 = __toESM(require("eventemitter3"));
|
|
var NDKNostrRpc = class extends import_eventemitter36.default {
|
|
ndk;
|
|
signer;
|
|
debug;
|
|
constructor(ndk, signer, debug4) {
|
|
super();
|
|
this.ndk = ndk;
|
|
this.signer = signer;
|
|
this.debug = debug4.extend("rpc");
|
|
}
|
|
/**
|
|
* Subscribe to a filter. This function will resolve once the subscription is ready.
|
|
*/
|
|
async subscribe(filter) {
|
|
const sub = this.ndk.subscribe(filter, { closeOnEose: false });
|
|
sub.on("event", async (event) => {
|
|
try {
|
|
const parsedEvent = await this.parseEvent(event);
|
|
if (parsedEvent.method) {
|
|
this.emit("request", parsedEvent);
|
|
} else {
|
|
this.emit(`response-${parsedEvent.id}`, parsedEvent);
|
|
}
|
|
} catch (e) {
|
|
this.debug("error parsing event", e, event);
|
|
}
|
|
});
|
|
return new Promise((resolve, reject) => {
|
|
sub.on("eose", () => resolve(sub));
|
|
});
|
|
}
|
|
async parseEvent(event) {
|
|
const remoteUser = this.ndk.getUser({ hexpubkey: event.pubkey });
|
|
remoteUser.ndk = this.ndk;
|
|
const decryptedContent = await this.signer.decrypt(remoteUser, event.content);
|
|
const parsedContent = JSON.parse(decryptedContent);
|
|
const { id, method, params, result, error } = parsedContent;
|
|
if (method) {
|
|
return { id, pubkey: event.pubkey, method, params, event };
|
|
} else {
|
|
return { id, result, error, event };
|
|
}
|
|
}
|
|
async sendResponse(id, remotePubkey, result, kind = 24133, error) {
|
|
const res = { id, result };
|
|
if (error) {
|
|
res.error = error;
|
|
}
|
|
const localUser = await this.signer.user();
|
|
const remoteUser = this.ndk.getUser({ hexpubkey: remotePubkey });
|
|
const event = new NDKEvent(this.ndk, {
|
|
kind,
|
|
content: JSON.stringify(res),
|
|
tags: [["p", remotePubkey]],
|
|
pubkey: localUser.hexpubkey
|
|
});
|
|
event.content = await this.signer.encrypt(remoteUser, event.content);
|
|
await event.sign(this.signer);
|
|
await event.publish();
|
|
}
|
|
/**
|
|
* Sends a request.
|
|
* @param remotePubkey
|
|
* @param method
|
|
* @param params
|
|
* @param kind
|
|
* @param id
|
|
*/
|
|
async sendRequest(remotePubkey, method, params = [], kind = 24133, cb) {
|
|
const id = Math.random().toString(36).substring(7);
|
|
const localUser = await this.signer.user();
|
|
const remoteUser = this.ndk.getUser({ hexpubkey: remotePubkey });
|
|
const request = { id, method, params };
|
|
const promise = new Promise((resolve) => {
|
|
if (cb)
|
|
this.once(`response-${id}`, cb);
|
|
});
|
|
const event = new NDKEvent(this.ndk, {
|
|
kind,
|
|
content: JSON.stringify(request),
|
|
tags: [["p", remotePubkey]],
|
|
pubkey: localUser.hexpubkey
|
|
});
|
|
event.content = await this.signer.encrypt(remoteUser, event.content);
|
|
await event.sign(this.signer);
|
|
this.debug(`sending ${method} request to`, remotePubkey);
|
|
await event.publish();
|
|
return promise;
|
|
}
|
|
};
|
|
|
|
// src/signers/nip46/backend/connect.ts
|
|
var ConnectEventHandlingStrategy = class {
|
|
async handle(backend, remotePubkey, params) {
|
|
const [pubkey, token] = params;
|
|
const debug4 = backend.debug.extend("connect");
|
|
debug4(`connection request from ${pubkey}`);
|
|
if (token && backend.applyToken) {
|
|
debug4(`applying token`);
|
|
await backend.applyToken(pubkey, token);
|
|
}
|
|
if (await backend.pubkeyAllowed(pubkey, "connect", token)) {
|
|
debug4(`connection request from ${pubkey} allowed`);
|
|
return "ack";
|
|
} else {
|
|
debug4(`connection request from ${pubkey} rejected`);
|
|
}
|
|
return void 0;
|
|
}
|
|
};
|
|
|
|
// src/signers/nip46/backend/describe.ts
|
|
var DescribeHandlingStrategy = class {
|
|
async handle(backend, remotePubkey, params) {
|
|
const keys = Object.keys(backend.handlers);
|
|
return JSON.stringify(keys);
|
|
}
|
|
};
|
|
|
|
// src/signers/nip46/backend/get-public-key.ts
|
|
var GetPublicKeyHandlingStrategy = class {
|
|
async handle(backend, remotePubkey, params) {
|
|
return backend.localUser?.hexpubkey;
|
|
}
|
|
};
|
|
|
|
// src/signers/nip46/backend/nip04-decrypt.ts
|
|
var Nip04DecryptHandlingStrategy = class {
|
|
async handle(backend, remotePubkey, params) {
|
|
const [senderPubkey, payload] = params;
|
|
const senderUser = new NDKUser({ hexpubkey: senderPubkey });
|
|
const decryptedPayload = await backend.decrypt(remotePubkey, senderUser, payload);
|
|
return JSON.stringify([decryptedPayload]);
|
|
}
|
|
};
|
|
|
|
// src/signers/nip46/backend/nip04-encrypt.ts
|
|
var Nip04EncryptHandlingStrategy = class {
|
|
async handle(backend, remotePubkey, params) {
|
|
const [recipientPubkey, payload] = params;
|
|
const recipientUser = new NDKUser({ hexpubkey: recipientPubkey });
|
|
const decryptedPayload = await backend.encrypt(remotePubkey, recipientUser, payload);
|
|
return decryptedPayload;
|
|
}
|
|
};
|
|
|
|
// src/signers/nip46/backend/sign-event.ts
|
|
var SignEventHandlingStrategy = class {
|
|
async handle(backend, remotePubkey, params) {
|
|
const event = await backend.signEvent(remotePubkey, params);
|
|
if (!event)
|
|
return void 0;
|
|
return JSON.stringify(await event.toNostrEvent());
|
|
}
|
|
};
|
|
|
|
// src/signers/nip46/backend/index.ts
|
|
var NDKNip46Backend = class {
|
|
ndk;
|
|
signer;
|
|
localUser;
|
|
debug;
|
|
rpc;
|
|
permitCallback;
|
|
/**
|
|
* @param ndk The NDK instance to use
|
|
* @param privateKey The private key of the npub that wants to be published as
|
|
*/
|
|
constructor(ndk, privateKey, permitCallback) {
|
|
this.ndk = ndk;
|
|
this.signer = new NDKPrivateKeySigner(privateKey);
|
|
this.debug = ndk.debug.extend("nip46:backend");
|
|
this.rpc = new NDKNostrRpc(ndk, this.signer, this.debug);
|
|
this.permitCallback = permitCallback;
|
|
}
|
|
/**
|
|
* This method starts the backend, which will start listening for incoming
|
|
* requests.
|
|
*/
|
|
async start() {
|
|
this.localUser = await this.signer.user();
|
|
const sub = this.ndk.subscribe(
|
|
{
|
|
kinds: [24133],
|
|
"#p": [this.localUser.hexpubkey]
|
|
},
|
|
{ closeOnEose: false }
|
|
);
|
|
sub.on("event", (e) => this.handleIncomingEvent(e));
|
|
}
|
|
handlers = {
|
|
connect: new ConnectEventHandlingStrategy(),
|
|
sign_event: new SignEventHandlingStrategy(),
|
|
nip04_encrypt: new Nip04EncryptHandlingStrategy(),
|
|
nip04_decrypt: new Nip04DecryptHandlingStrategy(),
|
|
get_public_key: new GetPublicKeyHandlingStrategy(),
|
|
describe: new DescribeHandlingStrategy()
|
|
};
|
|
/**
|
|
* Enables the user to set a custom strategy for handling incoming events.
|
|
* @param method - The method to set the strategy for
|
|
* @param strategy - The strategy to set
|
|
*/
|
|
setStrategy(method, strategy) {
|
|
this.handlers[method] = strategy;
|
|
}
|
|
/**
|
|
* Overload this method to apply tokens, which can
|
|
* wrap permission sets to be applied to a pubkey.
|
|
* @param pubkey public key to apply token to
|
|
* @param token token to apply
|
|
*/
|
|
async applyToken(pubkey, token) {
|
|
throw new Error("connection token not supported");
|
|
}
|
|
async handleIncomingEvent(event) {
|
|
const { id, method, params } = await this.rpc.parseEvent(event);
|
|
const remotePubkey = event.pubkey;
|
|
let response;
|
|
this.debug("incoming event", { id, method, params });
|
|
if (!(0, import_nostr_tools11.verifySignature)(event.rawEvent())) {
|
|
this.debug("invalid signature", event.rawEvent());
|
|
return;
|
|
}
|
|
const strategy = this.handlers[method];
|
|
if (strategy) {
|
|
try {
|
|
response = await strategy.handle(this, remotePubkey, params);
|
|
} catch (e) {
|
|
this.debug("error handling event", e, { id, method, params });
|
|
this.rpc.sendResponse(id, remotePubkey, "error", void 0, e.message);
|
|
}
|
|
} else {
|
|
this.debug("unsupported method", { method, params });
|
|
}
|
|
if (response) {
|
|
this.debug(`sending response to ${remotePubkey}`, response);
|
|
this.rpc.sendResponse(id, remotePubkey, response);
|
|
} else {
|
|
this.rpc.sendResponse(id, remotePubkey, "error", void 0, "Not authorized");
|
|
}
|
|
}
|
|
async decrypt(remotePubkey, senderUser, payload) {
|
|
if (!await this.pubkeyAllowed(remotePubkey, "decrypt", payload)) {
|
|
this.debug(`decrypt request from ${remotePubkey} rejected`);
|
|
return void 0;
|
|
}
|
|
return await this.signer.decrypt(senderUser, payload);
|
|
}
|
|
async encrypt(remotePubkey, recipientUser, payload) {
|
|
if (!await this.pubkeyAllowed(remotePubkey, "encrypt", payload)) {
|
|
this.debug(`encrypt request from ${remotePubkey} rejected`);
|
|
return void 0;
|
|
}
|
|
return await this.signer.encrypt(recipientUser, payload);
|
|
}
|
|
async signEvent(remotePubkey, params) {
|
|
const [eventString] = params;
|
|
this.debug(`sign event request from ${remotePubkey}`);
|
|
const event = new NDKEvent(this.ndk, JSON.parse(eventString));
|
|
this.debug("event to sign", event.rawEvent());
|
|
if (!await this.pubkeyAllowed(remotePubkey, "sign_event", event)) {
|
|
this.debug(`sign event request from ${remotePubkey} rejected`);
|
|
return void 0;
|
|
}
|
|
this.debug(`sign event request from ${remotePubkey} allowed`);
|
|
await event.sign(this.signer);
|
|
return event;
|
|
}
|
|
/**
|
|
* This method should be overriden by the user to allow or reject incoming
|
|
* connections.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
async pubkeyAllowed(pubkey, method, params) {
|
|
return this.permitCallback(pubkey, method, params);
|
|
}
|
|
};
|
|
|
|
// src/signers/nip46/index.ts
|
|
var NDKNip46Signer = class {
|
|
ndk;
|
|
remoteUser;
|
|
remotePubkey;
|
|
token;
|
|
localSigner;
|
|
rpc;
|
|
debug;
|
|
/**
|
|
* @param ndk - The NDK instance to use
|
|
* @param tokenOrRemotePubkey - The public key, or a connection token, of the npub that wants to be published as
|
|
* @param localSigner - The signer that will be used to request events to be signed
|
|
*/
|
|
constructor(ndk, tokenOrRemotePubkey, localSigner) {
|
|
let remotePubkey;
|
|
let token;
|
|
if (tokenOrRemotePubkey.includes("#")) {
|
|
const parts = tokenOrRemotePubkey.split("#");
|
|
remotePubkey = new NDKUser({ npub: parts[0] }).hexpubkey;
|
|
token = parts[1];
|
|
} else if (tokenOrRemotePubkey.startsWith("npub")) {
|
|
remotePubkey = new NDKUser({
|
|
npub: tokenOrRemotePubkey
|
|
}).hexpubkey;
|
|
} else {
|
|
remotePubkey = tokenOrRemotePubkey;
|
|
}
|
|
this.ndk = ndk;
|
|
this.remotePubkey = remotePubkey;
|
|
this.token = token;
|
|
this.debug = ndk.debug.extend("nip46:signer");
|
|
this.remoteUser = new NDKUser({ hexpubkey: remotePubkey });
|
|
if (!localSigner) {
|
|
this.localSigner = NDKPrivateKeySigner.generate();
|
|
} else {
|
|
this.localSigner = localSigner;
|
|
}
|
|
this.rpc = new NDKNostrRpc(ndk, this.localSigner, this.debug);
|
|
}
|
|
/**
|
|
* Get the user that is being published as
|
|
*/
|
|
async user() {
|
|
return this.remoteUser;
|
|
}
|
|
async blockUntilReady() {
|
|
const localUser = await this.localSigner.user();
|
|
const user = this.ndk.getUser({ npub: localUser.npub });
|
|
await this.rpc.subscribe({
|
|
kinds: [24133],
|
|
"#p": [localUser.hexpubkey]
|
|
});
|
|
return new Promise((resolve, reject) => {
|
|
setTimeout(() => {
|
|
const connectParams = [localUser.hexpubkey];
|
|
if (this.token) {
|
|
connectParams.push(this.token);
|
|
}
|
|
this.rpc.sendRequest(
|
|
this.remotePubkey,
|
|
"connect",
|
|
connectParams,
|
|
24133,
|
|
(response) => {
|
|
if (response.result === "ack") {
|
|
resolve(user);
|
|
} else {
|
|
reject(response.error);
|
|
}
|
|
}
|
|
);
|
|
}, 100);
|
|
});
|
|
}
|
|
async encrypt(recipient, value) {
|
|
this.debug("asking for encryption");
|
|
const promise = new Promise((resolve, reject) => {
|
|
this.rpc.sendRequest(
|
|
this.remotePubkey,
|
|
"nip04_encrypt",
|
|
[recipient.hexpubkey, value],
|
|
24133,
|
|
(response) => {
|
|
if (!response.error) {
|
|
resolve(response.result);
|
|
} else {
|
|
reject(response.error);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
return promise;
|
|
}
|
|
async decrypt(sender, value) {
|
|
this.debug("asking for decryption");
|
|
const promise = new Promise((resolve, reject) => {
|
|
this.rpc.sendRequest(
|
|
this.remotePubkey,
|
|
"nip04_decrypt",
|
|
[sender.hexpubkey, value],
|
|
24133,
|
|
(response) => {
|
|
if (!response.error) {
|
|
const value2 = JSON.parse(response.result);
|
|
resolve(value2[0]);
|
|
} else {
|
|
reject(response.error);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
return promise;
|
|
}
|
|
async sign(event) {
|
|
this.debug("asking for a signature");
|
|
const promise = new Promise((resolve, reject) => {
|
|
this.rpc.sendRequest(
|
|
this.remotePubkey,
|
|
"sign_event",
|
|
[JSON.stringify(event)],
|
|
24133,
|
|
(response) => {
|
|
this.debug("got a response", response);
|
|
if (!response.error) {
|
|
const json = JSON.parse(response.result);
|
|
resolve(json.sig);
|
|
} else {
|
|
reject(response.error);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
return promise;
|
|
}
|
|
};
|
|
|
|
// src/ndk/index.ts
|
|
var import_debug3 = __toESM(require("debug"));
|
|
var import_eventemitter39 = __toESM(require("eventemitter3"));
|
|
|
|
// src/events/dedup.ts
|
|
function dedup(event1, event2) {
|
|
if (event1.created_at > event2.created_at) {
|
|
return event1;
|
|
}
|
|
return event2;
|
|
}
|
|
|
|
// src/outbox/tracker.ts
|
|
var import_eventemitter37 = __toESM(require("eventemitter3"));
|
|
var import_typescript_lru_cache = require("typescript-lru-cache");
|
|
var OutboxItem = class {
|
|
/**
|
|
* Type of item
|
|
*/
|
|
type;
|
|
/**
|
|
* The relay URLs that are of interest to this item
|
|
*/
|
|
relayUrlScores;
|
|
readRelays;
|
|
writeRelays;
|
|
constructor(type) {
|
|
this.type = type;
|
|
this.relayUrlScores = /* @__PURE__ */ new Map();
|
|
this.readRelays = /* @__PURE__ */ new Set();
|
|
this.writeRelays = /* @__PURE__ */ new Set();
|
|
}
|
|
};
|
|
var OutboxTracker = class extends import_eventemitter37.default {
|
|
data;
|
|
ndk;
|
|
debug;
|
|
constructor(ndk) {
|
|
super();
|
|
this.ndk = ndk;
|
|
this.debug = ndk.debug.extend("outbox-tracker");
|
|
this.data = new import_typescript_lru_cache.LRUCache({
|
|
maxSize: 1e5,
|
|
entryExpirationTimeInMS: 5e3
|
|
});
|
|
}
|
|
trackUsers(items) {
|
|
for (const item of items) {
|
|
const itemKey = getKeyFromItem(item);
|
|
if (this.data.has(itemKey))
|
|
continue;
|
|
const outboxItem = this.track(item, "user");
|
|
const user = item instanceof NDKUser ? item : new NDKUser({ hexpubkey: item });
|
|
user.ndk = this.ndk;
|
|
user.relayList().then((relayList) => {
|
|
if (relayList) {
|
|
outboxItem.readRelays = new Set(relayList.readRelayUrls);
|
|
outboxItem.writeRelays = new Set(relayList.writeRelayUrls);
|
|
for (const relayUrl of outboxItem.readRelays) {
|
|
if (this.ndk.pool.blacklistRelayUrls.has(relayUrl)) {
|
|
this.debug(`removing blacklisted relay ${relayUrl} from read relays`);
|
|
outboxItem.readRelays.delete(relayUrl);
|
|
}
|
|
}
|
|
for (const relayUrl of outboxItem.writeRelays) {
|
|
if (this.ndk.pool.blacklistRelayUrls.has(relayUrl)) {
|
|
this.debug(`removing blacklisted relay ${relayUrl} from write relays`);
|
|
outboxItem.writeRelays.delete(relayUrl);
|
|
}
|
|
}
|
|
this.data.set(itemKey, outboxItem);
|
|
this.debug(
|
|
`Adding ${outboxItem.readRelays.size} read relays and ${outboxItem.writeRelays.size} write relays for ${user.hexpubkey}`
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param key
|
|
* @param score
|
|
*/
|
|
track(item, type) {
|
|
const key = getKeyFromItem(item);
|
|
type ??= getTypeFromItem(item);
|
|
let outboxItem = this.data.get(key);
|
|
if (!outboxItem)
|
|
outboxItem = new OutboxItem(type);
|
|
this.data.set(key, outboxItem);
|
|
return outboxItem;
|
|
}
|
|
};
|
|
function getKeyFromItem(item) {
|
|
if (item instanceof NDKUser) {
|
|
return item.hexpubkey;
|
|
} else {
|
|
return item;
|
|
}
|
|
}
|
|
function getTypeFromItem(item) {
|
|
if (item instanceof NDKUser) {
|
|
return "user";
|
|
} else {
|
|
return "kind";
|
|
}
|
|
}
|
|
|
|
// src/relay/pool/index.ts
|
|
var import_eventemitter38 = __toESM(require("eventemitter3"));
|
|
var NDKPool = class extends import_eventemitter38.default {
|
|
// TODO: This should probably be an LRU cache
|
|
relays = /* @__PURE__ */ new Map();
|
|
blacklistRelayUrls;
|
|
debug;
|
|
temporaryRelayTimers = /* @__PURE__ */ new Map();
|
|
flappingRelays = /* @__PURE__ */ new Set();
|
|
// A map to store timeouts for each flapping relay.
|
|
backoffTimes = /* @__PURE__ */ new Map();
|
|
constructor(relayUrls = [], blacklistedRelayUrls = [], ndk, debug4) {
|
|
super();
|
|
this.debug = debug4 ?? ndk.debug.extend("pool");
|
|
for (const relayUrl of relayUrls) {
|
|
const relay = new NDKRelay(relayUrl);
|
|
this.addRelay(relay, false);
|
|
}
|
|
this.blacklistRelayUrls = new Set(blacklistedRelayUrls);
|
|
}
|
|
/**
|
|
* Adds a relay to the pool, and sets a timer to remove it if it is not used within the specified time.
|
|
* @param relay - The relay to add to the pool.
|
|
* @param removeIfUnusedAfter - The time in milliseconds to wait before removing the relay from the pool after it is no longer used.
|
|
*/
|
|
useTemporaryRelay(relay, removeIfUnusedAfter = 6e5) {
|
|
const relayAlreadyInPool = this.relays.has(relay.url);
|
|
if (!relayAlreadyInPool) {
|
|
this.addRelay(relay);
|
|
}
|
|
const existingTimer = this.temporaryRelayTimers.get(relay.url);
|
|
if (existingTimer) {
|
|
clearTimeout(existingTimer);
|
|
}
|
|
if (!relayAlreadyInPool || existingTimer) {
|
|
const timer = setTimeout(() => {
|
|
this.removeRelay(relay.url);
|
|
}, removeIfUnusedAfter);
|
|
this.temporaryRelayTimers.set(relay.url, timer);
|
|
}
|
|
}
|
|
/**
|
|
* Adds a relay to the pool.
|
|
*
|
|
* @param relay - The relay to add to the pool.
|
|
* @param connect - Whether or not to connect to the relay.
|
|
*/
|
|
addRelay(relay, connect = true) {
|
|
const relayUrl = relay.url;
|
|
if (this.blacklistRelayUrls?.has(relayUrl)) {
|
|
this.debug(`Relay ${relayUrl} is blacklisted`);
|
|
return;
|
|
}
|
|
relay.on("notice", (relay2, notice) => this.emit("notice", relay2, notice));
|
|
relay.on("connect", () => this.handleRelayConnect(relayUrl));
|
|
relay.on("disconnect", () => this.emit("relay:disconnect", relay));
|
|
relay.on("flapping", () => this.handleFlapping(relay));
|
|
this.relays.set(relayUrl, relay);
|
|
if (connect) {
|
|
relay.connect().catch((e) => {
|
|
this.debug(`Failed to connect to relay ${relayUrl}`, e);
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Removes a relay from the pool.
|
|
* @param relayUrl - The URL of the relay to remove.
|
|
* @returns {boolean} True if the relay was removed, false if it was not found.
|
|
*/
|
|
removeRelay(relayUrl) {
|
|
const relay = this.relays.get(relayUrl);
|
|
if (relay) {
|
|
relay.disconnect();
|
|
this.relays.delete(relayUrl);
|
|
this.emit("relay:disconnect", relay);
|
|
return true;
|
|
}
|
|
const existingTimer = this.temporaryRelayTimers.get(relayUrl);
|
|
if (existingTimer) {
|
|
clearTimeout(existingTimer);
|
|
this.temporaryRelayTimers.delete(relayUrl);
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Fetches a relay from the pool, or creates a new one if it does not exist.
|
|
*
|
|
* New relays will be attempted to be connected.
|
|
*/
|
|
getRelay(url) {
|
|
let relay = this.relays.get(url);
|
|
if (!relay) {
|
|
relay = new NDKRelay(url);
|
|
this.addRelay(relay);
|
|
}
|
|
return relay;
|
|
}
|
|
handleRelayConnect(relayUrl) {
|
|
this.debug(`Relay ${relayUrl} connected`);
|
|
this.emit("relay:connect", this.relays.get(relayUrl));
|
|
if (this.stats().connected === this.relays.size) {
|
|
this.emit("connect");
|
|
}
|
|
}
|
|
/**
|
|
* Attempts to establish a connection to each relay in the pool.
|
|
*
|
|
* @async
|
|
* @param {number} [timeoutMs] - Optional timeout in milliseconds for each connection attempt.
|
|
* @returns {Promise<void>} A promise that resolves when all connection attempts have completed.
|
|
* @throws {Error} If any of the connection attempts result in an error or timeout.
|
|
*/
|
|
async connect(timeoutMs) {
|
|
const promises = [];
|
|
this.debug(
|
|
`Connecting to ${this.relays.size} relays${timeoutMs ? `, timeout ${timeoutMs}...` : ""}`
|
|
);
|
|
for (const relay of this.relays.values()) {
|
|
if (timeoutMs) {
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
setTimeout(() => reject(`Timed out after ${timeoutMs}ms`), timeoutMs);
|
|
});
|
|
promises.push(
|
|
Promise.race([relay.connect(), timeoutPromise]).catch((e) => {
|
|
this.debug(
|
|
`Failed to connect to relay ${relay.url}: ${e ?? "No reason specified"}`
|
|
);
|
|
})
|
|
);
|
|
} else {
|
|
promises.push(relay.connect());
|
|
}
|
|
}
|
|
if (timeoutMs) {
|
|
setTimeout(() => {
|
|
const allConnected = this.stats().connected === this.relays.size;
|
|
const someConnected = this.stats().connected > 0;
|
|
if (!allConnected && someConnected) {
|
|
this.emit("connect");
|
|
}
|
|
}, timeoutMs);
|
|
}
|
|
await Promise.all(promises);
|
|
}
|
|
checkOnFlappingRelays() {
|
|
const flappingRelaysCount = this.flappingRelays.size;
|
|
const totalRelays = this.relays.size;
|
|
if (flappingRelaysCount / totalRelays >= 0.8) {
|
|
for (const relayUrl of this.flappingRelays) {
|
|
this.backoffTimes.set(relayUrl, 0);
|
|
}
|
|
}
|
|
}
|
|
handleFlapping(relay) {
|
|
this.debug(`Relay ${relay.url} is flapping`);
|
|
let currentBackoff = this.backoffTimes.get(relay.url) || 5e3;
|
|
currentBackoff = currentBackoff * 2;
|
|
this.backoffTimes.set(relay.url, currentBackoff);
|
|
this.debug(`Backoff time for ${relay.url} is ${currentBackoff}ms`);
|
|
setTimeout(() => {
|
|
this.debug(`Attempting to reconnect to ${relay.url}`);
|
|
relay.connect();
|
|
this.checkOnFlappingRelays();
|
|
}, currentBackoff);
|
|
relay.disconnect();
|
|
this.emit("flapping", relay);
|
|
}
|
|
size() {
|
|
return this.relays.size;
|
|
}
|
|
/**
|
|
* Returns the status of each relay in the pool.
|
|
* @returns {NDKPoolStats} An object containing the number of relays in each status.
|
|
*/
|
|
stats() {
|
|
const stats = {
|
|
total: 0,
|
|
connected: 0,
|
|
disconnected: 0,
|
|
connecting: 0
|
|
};
|
|
for (const relay of this.relays.values()) {
|
|
stats.total++;
|
|
if (relay.status === 1 /* CONNECTED */) {
|
|
stats.connected++;
|
|
} else if (relay.status === 3 /* DISCONNECTED */) {
|
|
stats.disconnected++;
|
|
} else if (relay.status === 0 /* CONNECTING */) {
|
|
stats.connecting++;
|
|
}
|
|
}
|
|
return stats;
|
|
}
|
|
connectedRelays() {
|
|
return Array.from(this.relays.values()).filter(
|
|
(relay) => relay.status === 1 /* CONNECTED */
|
|
);
|
|
}
|
|
/**
|
|
* Get a list of all relay urls in the pool.
|
|
*/
|
|
urls() {
|
|
return Array.from(this.relays.keys());
|
|
}
|
|
};
|
|
|
|
// src/relay/sets/utils.ts
|
|
function correctRelaySet(relaySet, pool) {
|
|
const connectedRelays = pool.connectedRelays();
|
|
const includesConnectedRelay = Array.from(relaySet.relays).some((relay) => {
|
|
return connectedRelays.map((r) => r.url).includes(relay.url);
|
|
});
|
|
if (!includesConnectedRelay) {
|
|
for (const relay of connectedRelays) {
|
|
relaySet.addRelay(relay);
|
|
}
|
|
}
|
|
if (connectedRelays.length === 0) {
|
|
for (const relay of pool.relays.values()) {
|
|
relaySet.addRelay(relay);
|
|
}
|
|
}
|
|
return relaySet;
|
|
}
|
|
|
|
// src/ndk/index.ts
|
|
var DEFAULT_OUTBOX_RELAYS = ["wss://purplepag.es", "wss://relay.snort.social"];
|
|
var DEFAULT_BLACKLISTED_RELAYS = [
|
|
"wss://brb.io"
|
|
// BRB
|
|
];
|
|
var NDK = class extends import_eventemitter39.default {
|
|
explicitRelayUrls;
|
|
pool;
|
|
outboxPool;
|
|
_signer;
|
|
_activeUser;
|
|
cacheAdapter;
|
|
debug;
|
|
devWriteRelaySet;
|
|
outboxTracker;
|
|
mutedIds;
|
|
autoConnectUserRelays = true;
|
|
autoFetchUserMutelist = true;
|
|
constructor(opts = {}) {
|
|
super();
|
|
this.debug = opts.debug || (0, import_debug3.default)("ndk");
|
|
this.explicitRelayUrls = opts.explicitRelayUrls;
|
|
this.pool = new NDKPool(opts.explicitRelayUrls || [], opts.blacklistRelayUrls, this);
|
|
this.debug(`Starting with explicit relays: ${JSON.stringify(this.explicitRelayUrls)}`);
|
|
this.autoConnectUserRelays = opts.autoConnectUserRelays ?? true;
|
|
this.autoFetchUserMutelist = opts.autoFetchUserMutelist ?? true;
|
|
if (opts.enableOutboxModel) {
|
|
this.outboxPool = new NDKPool(
|
|
opts.outboxRelayUrls || DEFAULT_OUTBOX_RELAYS,
|
|
opts.blacklistRelayUrls || DEFAULT_BLACKLISTED_RELAYS,
|
|
this,
|
|
this.debug.extend("outbox-pool")
|
|
);
|
|
this.outboxTracker = new OutboxTracker(this);
|
|
}
|
|
this.signer = opts.signer;
|
|
this.cacheAdapter = opts.cacheAdapter;
|
|
this.mutedIds = opts.mutedIds || /* @__PURE__ */ new Map();
|
|
if (opts.devWriteRelayUrls) {
|
|
this.devWriteRelaySet = NDKRelaySet.fromRelayUrls(opts.devWriteRelayUrls, this);
|
|
}
|
|
}
|
|
toJSON() {
|
|
return { relayCount: this.pool.relays.size }.toString();
|
|
}
|
|
get activeUser() {
|
|
return this._activeUser;
|
|
}
|
|
/**
|
|
* Sets the active user for this NDK instance, typically this will be
|
|
* called when assigning a signer to the NDK instance.
|
|
*
|
|
* This function will automatically connect to the user's relays if
|
|
* `autoConnectUserRelays` is set to true.
|
|
*
|
|
* It will also fetch the user's mutelist if `autoFetchUserMutelist` is set to true.
|
|
*/
|
|
set activeUser(user) {
|
|
const differentUser = this._activeUser !== user;
|
|
this._activeUser = user;
|
|
if (user && differentUser) {
|
|
const connectToUserRelays = async (user2) => {
|
|
const relayList = await user2.relayList();
|
|
if (!relayList) {
|
|
this.debug("No relay list found for user", { npub: user2.npub });
|
|
return;
|
|
}
|
|
this.debug("Connecting to user relays", {
|
|
npub: user2.npub,
|
|
relays: relayList.relays
|
|
});
|
|
for (const url of relayList.relays) {
|
|
let relay = this.pool.relays.get(url);
|
|
if (!relay) {
|
|
relay = new NDKRelay(url);
|
|
this.pool.addRelay(relay);
|
|
}
|
|
}
|
|
};
|
|
const fetchUserMuteList = async (user2) => {
|
|
const muteLists = await this.fetchEvents([
|
|
{ kinds: [1e4 /* MuteList */], authors: [user2.pubkey] },
|
|
{
|
|
kinds: [3e4 /* CategorizedPeopleList */],
|
|
authors: [user2.pubkey],
|
|
"#d": ["mute"],
|
|
limit: 1
|
|
}
|
|
]);
|
|
if (!muteLists) {
|
|
this.debug("No mute list found for user", { npub: user2.npub });
|
|
return;
|
|
}
|
|
for (const muteList of muteLists) {
|
|
const list = lists_default.from(muteList);
|
|
for (const item of list.items) {
|
|
this.mutedIds.set(item[1], item[0]);
|
|
}
|
|
}
|
|
};
|
|
const userFunctions = [];
|
|
if (this.autoConnectUserRelays)
|
|
userFunctions.push(connectToUserRelays);
|
|
if (this.autoFetchUserMutelist)
|
|
userFunctions.push(fetchUserMuteList);
|
|
const runUserFunctions = async (user2) => {
|
|
for (const fn of userFunctions) {
|
|
await fn(user2);
|
|
}
|
|
};
|
|
const pool = this.outboxPool || this.pool;
|
|
if (pool.connectedRelays.length > 0) {
|
|
runUserFunctions(user);
|
|
} else {
|
|
this.debug("Waiting for connection to main relays");
|
|
pool.once("relay:connect", (relay) => {
|
|
this.debug("New relay came online", relay);
|
|
runUserFunctions(user);
|
|
});
|
|
}
|
|
} else if (!user) {
|
|
this.mutedIds = /* @__PURE__ */ new Map();
|
|
}
|
|
}
|
|
get signer() {
|
|
return this._signer;
|
|
}
|
|
set signer(newSigner) {
|
|
this._signer = newSigner;
|
|
this.debug(`setting signer`, this.autoConnectUserRelays);
|
|
newSigner?.user().then((user) => {
|
|
user.ndk = this;
|
|
this.activeUser = user;
|
|
});
|
|
}
|
|
/**
|
|
* Connect to relays with optional timeout.
|
|
* If the timeout is reached, the connection will be continued to be established in the background.
|
|
*/
|
|
async connect(timeoutMs) {
|
|
const connections = [this.pool.connect(timeoutMs)];
|
|
if (this.outboxPool) {
|
|
connections.push(this.outboxPool.connect(timeoutMs));
|
|
}
|
|
this.debug("Connecting to relays", { timeoutMs });
|
|
return Promise.allSettled(connections).then(() => {
|
|
});
|
|
}
|
|
/**
|
|
* Get a NDKUser object
|
|
*
|
|
* @param opts
|
|
* @returns
|
|
*/
|
|
getUser(opts) {
|
|
const user = new NDKUser(opts);
|
|
user.ndk = this;
|
|
return user;
|
|
}
|
|
/**
|
|
* Create a new subscription. Subscriptions automatically start, you can make them automatically close when all relays send back an EOSE by setting `opts.closeOnEose` to `true`)
|
|
*
|
|
* @param filters
|
|
* @param opts
|
|
* @param relaySet explicit relay set to use
|
|
* @param autoStart automatically start the subscription
|
|
* @returns NDKSubscription
|
|
*/
|
|
subscribe(filters, opts, relaySet, autoStart = true) {
|
|
const subscription = new NDKSubscription(this, filters, opts, relaySet);
|
|
if (relaySet) {
|
|
for (const relay of relaySet.relays) {
|
|
this.pool.useTemporaryRelay(relay);
|
|
}
|
|
}
|
|
if (this.outboxPool && subscription.hasAuthorsFilter()) {
|
|
const authors = subscription.filters.filter((filter) => filter.authors && filter.authors?.length > 0).map((filter) => filter.authors).flat();
|
|
this.outboxTracker?.trackUsers(authors);
|
|
}
|
|
if (autoStart)
|
|
subscription.start();
|
|
return subscription;
|
|
}
|
|
/**
|
|
* Publish an event to a relay
|
|
* @param event event to publish
|
|
* @param relaySet explicit relay set to use
|
|
* @param timeoutMs timeout in milliseconds to wait for the event to be published
|
|
* @returns The relays the event was published to
|
|
*
|
|
* @deprecated Use `event.publish()` instead
|
|
*/
|
|
async publish(event, relaySet, timeoutMs) {
|
|
this.debug("Deprecated: Use `event.publish()` instead");
|
|
return event.publish(relaySet, timeoutMs);
|
|
}
|
|
/**
|
|
* Fetch a single event.
|
|
*
|
|
* @param idOrFilter event id in bech32 format or filter
|
|
* @param opts subscription options
|
|
* @param relaySet explicit relay set to use
|
|
*/
|
|
async fetchEvent(idOrFilter, opts, relaySet) {
|
|
let filter;
|
|
if (!relaySet && typeof idOrFilter === "string") {
|
|
if (!isNip33AValue(idOrFilter)) {
|
|
const relays = relaysFromBech32(idOrFilter);
|
|
if (relays.length > 0) {
|
|
relaySet = new NDKRelaySet(new Set(relays), this);
|
|
relaySet = correctRelaySet(relaySet, this.pool);
|
|
}
|
|
}
|
|
}
|
|
if (typeof idOrFilter === "string") {
|
|
filter = filterFromId(idOrFilter);
|
|
} else {
|
|
filter = idOrFilter;
|
|
}
|
|
if (!filter) {
|
|
throw new Error(`Invalid filter: ${JSON.stringify(idOrFilter)}`);
|
|
}
|
|
return new Promise((resolve) => {
|
|
const s = this.subscribe(
|
|
filter,
|
|
{ ...opts || {}, closeOnEose: true },
|
|
relaySet,
|
|
false
|
|
);
|
|
s.on("event", (event) => {
|
|
event.ndk = this;
|
|
resolve(event);
|
|
});
|
|
s.on("eose", () => {
|
|
resolve(null);
|
|
});
|
|
s.start();
|
|
});
|
|
}
|
|
/**
|
|
* Fetch events
|
|
*/
|
|
async fetchEvents(filters, opts, relaySet) {
|
|
return new Promise((resolve) => {
|
|
const events = /* @__PURE__ */ new Map();
|
|
const relaySetSubscription = this.subscribe(
|
|
filters,
|
|
{ ...opts || {}, closeOnEose: true },
|
|
relaySet,
|
|
false
|
|
);
|
|
const onEvent = (event) => {
|
|
const dedupKey = event.deduplicationKey();
|
|
const existingEvent = events.get(dedupKey);
|
|
if (existingEvent) {
|
|
event = dedup(existingEvent, event);
|
|
}
|
|
event.ndk = this;
|
|
events.set(dedupKey, event);
|
|
};
|
|
relaySetSubscription.on("event", onEvent);
|
|
relaySetSubscription.on("event:dup", onEvent);
|
|
relaySetSubscription.on("eose", () => {
|
|
resolve(new Set(events.values()));
|
|
});
|
|
relaySetSubscription.start();
|
|
});
|
|
}
|
|
/**
|
|
* Ensures that a signer is available to sign an event.
|
|
*/
|
|
assertSigner() {
|
|
if (!this.signer) {
|
|
this.emit("signerRequired");
|
|
throw new Error("Signer required");
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/zap/invoice.ts
|
|
var import_light_bolt11_decoder = require("light-bolt11-decoder");
|
|
function zapInvoiceFromEvent(event) {
|
|
const description = event.getMatchingTags("description")[0];
|
|
const bolt11 = event.getMatchingTags("bolt11")[0];
|
|
let decodedInvoice;
|
|
let zapRequest;
|
|
if (!description || !bolt11 || !bolt11[1]) {
|
|
return null;
|
|
}
|
|
try {
|
|
let zapRequestPayload = description[1];
|
|
if (zapRequestPayload.startsWith("%")) {
|
|
zapRequestPayload = decodeURIComponent(zapRequestPayload);
|
|
}
|
|
if (zapRequestPayload === "") {
|
|
return null;
|
|
}
|
|
zapRequest = JSON.parse(zapRequestPayload);
|
|
decodedInvoice = (0, import_light_bolt11_decoder.decode)(bolt11[1]);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
const amountSection = decodedInvoice.sections.find((s) => s.name === "amount");
|
|
if (!amountSection) {
|
|
return null;
|
|
}
|
|
const amount = parseInt(amountSection.value);
|
|
if (!amount) {
|
|
return null;
|
|
}
|
|
const content = zapRequest.content;
|
|
const sender = zapRequest.pubkey;
|
|
const recipientTag = event.getMatchingTags("p")[0];
|
|
const recipient = recipientTag[1];
|
|
let zappedEvent = event.getMatchingTags("e")[0];
|
|
if (!zappedEvent) {
|
|
zappedEvent = event.getMatchingTags("a")[0];
|
|
}
|
|
const zappedEventId = zappedEvent ? zappedEvent[1] : void 0;
|
|
const zapInvoice = {
|
|
id: event.id,
|
|
zapper: event.pubkey,
|
|
zappee: sender,
|
|
zapped: recipient,
|
|
zappedEvent: zappedEventId,
|
|
amount,
|
|
comment: content
|
|
};
|
|
return zapInvoice;
|
|
}
|
|
// Annotate the CommonJS export names for ESM import in node:
|
|
0 && (module.exports = {
|
|
NDKAppHandlerEvent,
|
|
NDKArticle,
|
|
NDKDVMJobFeedback,
|
|
NDKDVMJobResult,
|
|
NDKDVMRequest,
|
|
NDKDvmJobFeedbackStatus,
|
|
NDKEvent,
|
|
NDKHighlight,
|
|
NDKKind,
|
|
NDKList,
|
|
NDKListKinds,
|
|
NDKNip07Signer,
|
|
NDKNip46Backend,
|
|
NDKNip46Signer,
|
|
NDKNostrRpc,
|
|
NDKPrivateKeySigner,
|
|
NDKRelay,
|
|
NDKRelayList,
|
|
NDKRelaySet,
|
|
NDKRelayStatus,
|
|
NDKRepost,
|
|
NDKSubscription,
|
|
NDKSubscriptionCacheUsage,
|
|
NDKTranscriptionDVM,
|
|
NDKUser,
|
|
calculateGroupableId,
|
|
defaultOpts,
|
|
mergeFilters,
|
|
profileFromEvent,
|
|
serializeProfile,
|
|
zapInvoiceFromEvent
|
|
});
|