@nostr-dev-kit/ndk
Version:
NDK - Nostr Development Kit
198 lines (177 loc) • 6.82 kB
text/typescript
import createDebug from "debug";
import type { NDKEvent } from "../../events/index.js";
import type { NDK } from "../../ndk/index.js";
import { chooseRelayCombinationForPubkeys } from "../../outbox/index.js";
import { getRelaysForFilterWithAuthors } from "../../outbox/read/with-authors.js";
import { getWriteRelaysFor } from "../../outbox/write.js";
import type { NDKFilter } from "../../subscription/index.js";
import type { Hexpubkey } from "../../user/index.js";
import { normalizeRelayUrl } from "../../utils/normalize-url.js";
import type { NDKRelay } from "../index.js";
import type { NDKPool } from "../pool/index.js";
import { NDKRelaySet } from "./index.js";
const d = createDebug("ndk:outbox:calculate");
/**
* Creates a NDKRelaySet for the specified event.
* TODO: account for relays where tagged pubkeys or hashtags
* tend to write to.
* @param ndk {NDK}
* @param event {Event}
* @returns Promise<NDKRelaySet>
*/
export async function calculateRelaySetFromEvent(
ndk: NDK,
event: NDKEvent,
requiredRelayCount?: number
): Promise<NDKRelaySet> {
const relays: Set<NDKRelay> = new Set();
// get the author's write relays
const authorWriteRelays = await getWriteRelaysFor(ndk, event.pubkey);
if (authorWriteRelays) {
authorWriteRelays.forEach((relayUrl) => {
const relay = ndk.pool?.getRelay(relayUrl);
if (relay) relays.add(relay);
});
}
// get all the hinted relays
let relayHints = event.tags
.filter((tag) => ["a", "e"].includes(tag[0]))
.map((tag) => tag[2])
// verify it's a valid URL
.filter((url: string | undefined) => url?.startsWith("wss://"))
.filter((url: string) => {
try {
new URL(url);
return true;
} catch {
return false;
}
})
.map((url: string) => normalizeRelayUrl(url));
// make unique
relayHints = Array.from(new Set(relayHints)).slice(0, 5);
relayHints.forEach((relayUrl) => {
const relay = ndk.pool?.getRelay(relayUrl, true, true);
if (relay) {
d("Adding relay hint %s", relayUrl);
relays.add(relay);
}
});
const pTags = event.getMatchingTags("p").map((tag) => tag[1]);
if (pTags.length < 5) {
const pTaggedRelays = Array.from(
chooseRelayCombinationForPubkeys(ndk, pTags, "read", {
preferredRelays: new Set(authorWriteRelays),
}).keys()
);
pTaggedRelays.forEach((relayUrl) => {
const relay = ndk.pool?.getRelay(relayUrl, false, true);
if (relay) {
d("Adding p-tagged relay %s", relayUrl);
relays.add(relay);
}
});
} else {
d("Too many p-tags to consider %d", pTags.length);
}
ndk.pool?.permanentAndConnectedRelays().forEach((relay: NDKRelay) => relays.add(relay));
// if we have less than the required relay count, add from ndk.explicitRelayUrls (skipping the ones that we already have)
if (requiredRelayCount && relays.size < requiredRelayCount) {
const explicitRelays = ndk.explicitRelayUrls
?.filter((url) => !Array.from(relays).some((r) => r.url === url))
.slice(0, requiredRelayCount - relays.size);
explicitRelays?.forEach((url) => {
const relay = ndk.pool?.getRelay(url, false, true);
if (relay) {
d("Adding explicit relay %s", url);
relays.add(relay);
}
});
}
return new NDKRelaySet(relays, ndk);
}
/**
* Creates a map of relay URLs that should receive a subset of the filter.
*
* The filter is broken up into the filter that each relay should receive.
* @param ndk
* @param filter
* @returns Promise<NDKRelaySet>
*/
export function calculateRelaySetsFromFilter(
ndk: NDK,
filters: NDKFilter[],
pool: NDKPool
): Map<WebSocket["url"], NDKFilter[]> {
const result = new Map<WebSocket["url"], NDKFilter[]>();
const authors = new Set<Hexpubkey>();
filters.forEach((filter) => {
if (filter.authors) {
filter.authors.forEach((author) => authors.add(author));
}
});
// if this filter has authors, get write relays for each
// one of them and add them to the map
if (authors.size > 0) {
const authorToRelaysMap = getRelaysForFilterWithAuthors(ndk, Array.from(authors));
// initialize all result with all the relayUrls we are going to return
for (const relayUrl of authorToRelaysMap.keys()) {
result.set(relayUrl, []);
}
// go through all the authorToRelaysMap and replace the authors of each filter with
// the resulting author set
for (const filter of filters) {
if (filter.authors) {
// replace authors with the authors for each relay
for (const [relayUrl, authors] of authorToRelaysMap.entries()) {
const authorFilterAndRelayPubkeyIntersection = filter.authors.filter((author) =>
authors.includes(author)
);
result.set(relayUrl, [
...result.get(relayUrl)!,
{
...filter,
// Overwrite authors sent to this relay with the authors that were
// present in the filter and are also present in the relay
authors: authorFilterAndRelayPubkeyIntersection,
},
]);
}
} else {
// if the filter doesn't have authors, add it to all relays
for (const relayUrl of authorToRelaysMap.keys()) {
result.set(relayUrl, [...result.get(relayUrl)!, filter]);
}
}
}
} else {
// If we don't, add the explicit relays
if (ndk.explicitRelayUrls) {
ndk.explicitRelayUrls.forEach((relayUrl) => {
result.set(relayUrl, filters);
});
}
}
if (result.size === 0) {
// If we don't have any relays, add all the permanent relays
pool.permanentAndConnectedRelays()
.slice(0, 5)
.forEach((relay) => {
result.set(relay.url, filters);
});
}
return result;
}
/**
* Calculates a number of RelaySets for each filter.
* @param ndk
* @param filters
*/
export function calculateRelaySetsFromFilters(
ndk: NDK,
filters: NDKFilter[],
pool: NDKPool
): Map<WebSocket["url"], NDKFilter[]> {
const a = calculateRelaySetsFromFilter(ndk, filters, pool);
return a;
}