Skip to content

Commit

Permalink
improve url normalization and be more careful on connection management
Browse files Browse the repository at this point in the history
  • Loading branch information
pablof7z committed Jun 25, 2024
1 parent f76ab71 commit e353c1c
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
* Optional content to use instead of the one from the event
*/
export let content = event?.content;
const markdownKinds = [ NDKKind.Article, 30818, 30041 ]
</script>

{#if event}
Expand All @@ -49,7 +51,7 @@
<Kind30000 {ndk} list={NDKList.from(event)} class={$$props.class} />
{:else if event.kind === 30001}
<Kind30001 {ndk} list={NDKList.from(event)} class={$$props.class} />
{:else if event.kind === 30023 || event.kind === 30818}
{:else if markdownKinds.includes(event.kind)}
<Kind30023
{ndk}
{content}
Expand Down
4 changes: 2 additions & 2 deletions ndk/src/events/fetch-tagged-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export async function fetchRootEvent(
if (!this.ndk) throw new Error("NDK instance not found");
const rootTag = getRootTag(this);
if (!rootTag) return undefined;
return this.ndk.fetchEventFromTag(rootTag, subOpts);
return this.ndk.fetchEventFromTag(rootTag, this, subOpts);
}

export async function fetchReplyEvent(
Expand All @@ -40,5 +40,5 @@ export async function fetchReplyEvent(
if (!this.ndk) throw new Error("NDK instance not found");
const replyTag = getReplyTag(this);
if (!replyTag) return undefined;
return this.ndk.fetchEventFromTag(replyTag, subOpts);
return this.ndk.fetchEventFromTag(replyTag, this, subOpts);
}
14 changes: 9 additions & 5 deletions ndk/src/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { NDKEventSerialized, deserialize, serialize } from "./serializer.js";
import { validate, verifySignature, getEventHash } from "./validation.js";
import { matchFilter } from "nostr-tools";

const skipClientTagOnKinds = [NDKKind.Contacts];

export type NDKEventId = string;
export type NDKTag = string[];

Expand Down Expand Up @@ -237,10 +239,9 @@ export class NDKEvent extends EventEmitter {
* @returns {NDKTag[]} An array of the matching tags
*/
public getMatchingTags(tagName: string, marker?: string): NDKTag[] {
const t = this.tags
.filter((tag) => tag[0] === tagName);

if (marker !== undefined) return t;
const t = this.tags.filter((tag) => tag[0] === tagName);

if (marker === undefined) return t;

return t.filter((tag) => tag[3] === marker);
}
Expand Down Expand Up @@ -420,7 +421,10 @@ export class NDKEvent extends EventEmitter {
}
}

if ((this.ndk?.clientName || this.ndk?.clientNip89)) {
if (
(this.ndk?.clientName || this.ndk?.clientNip89) &&
skipClientTagOnKinds.includes(this.kind!)
) {
if (!this.tags.some((tag) => tag[0] === "client")) {
const clientTag: NDKTag = ["client", this.ndk.clientName ?? ""];
if (this.ndk.clientNip89) clientTag.push(this.ndk.clientNip89);
Expand Down
8 changes: 5 additions & 3 deletions ndk/src/events/kinds/NDKRelayList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { NostrEvent } from "../index.js";
import { NDKEvent } from "../index.js";
import type { NDK } from "../../ndk/index.js";
import { NDKRelaySet } from "../../relay/sets/index.js";
import { normalizeRelayUrl } from "../../utils/normalize-url.js";
import { normalizeRelayUrl, tryNormalizeRelayUrl } from "../../utils/normalize-url.js";

const READ_MARKER = "read";
const WRITE_MARKER = "write";
Expand All @@ -26,7 +26,8 @@ export class NDKRelayList extends NDKEvent {
return this.tags
.filter((tag) => tag[0] === "r" || tag[0] === "relay")
.filter((tag) => !tag[2] || (tag[2] && tag[2] === READ_MARKER))
.map((tag) => normalizeRelayUrl(tag[1]));
.map((tag) => tryNormalizeRelayUrl(tag[1]))
.filter((url) => !!url) as WebSocket["url"][];
}

set readRelayUrls(relays: WebSocket["url"][]) {
Expand All @@ -39,7 +40,8 @@ export class NDKRelayList extends NDKEvent {
return this.tags
.filter((tag) => tag[0] === "r" || tag[0] === "relay")
.filter((tag) => !tag[2] || (tag[2] && tag[2] === WRITE_MARKER))
.map((tag) => normalizeRelayUrl(tag[1]));
.map((tag) => tryNormalizeRelayUrl(tag[1]))
.filter((url) => !!url) as WebSocket["url"][];
}

set writeRelayUrls(relays: WebSocket["url"][]) {
Expand Down
2 changes: 1 addition & 1 deletion ndk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ export { NDK as default, NDKConstructorParams } from "./ndk/index.js";
export { NDKZapInvoice, zapInvoiceFromEvent } from "./zap/invoice.js";
export * from "./zap/index.js";
export * from "./utils/normalize-url.js";
export * from './utils/get-users-relay-list.js';
export * from "./utils/get-users-relay-list.js";
21 changes: 21 additions & 0 deletions ndk/src/ndk/fetch-event-from-tag.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NDK } from ".";
import { NDKEvent } from "../events";
import { NDKSubscriptionCacheUsage, NDKSubscriptionOptions } from "../subscription";

const ndk = new NDK();

describe("fetchEventFromTag", () => {
describe("with subOpts specifying only cache", () => {
it("does not try to load a relay", async () => {
const originalEvent = new NDKEvent();
const tag = ["e", "id", "hint"];
originalEvent.tags.push(tag);
const subOpts: NDKSubscriptionOptions = {
cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE,
};
jest.spyOn(ndk.pool, "getRelay");
const event = await ndk.fetchEventFromTag(tag, originalEvent, subOpts);
expect(ndk.pool.getRelay).not.toHaveBeenCalled();
});
});
});
85 changes: 77 additions & 8 deletions ndk/src/ndk/fetch-event-from-tag.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NDK } from ".";
import { NDKEvent, NDKTag } from "../events";
import type { NDKRelaySet } from "../relay/sets";
import { getWriteRelaysFor } from "../outbox/read/with-authors";
import { NDKRelaySet } from "../relay/sets";
import { calculateRelaySetsFromFilters } from "../relay/sets/calculate";
import { NDKSubscriptionOptions } from "../subscription";

/**
Expand Down Expand Up @@ -35,31 +37,98 @@ export type NDKFetchFallbackOptions = {
timeout?: number;
};

function isValidHint(hint: string | undefined) {
if (!hint || hint === "") return false;

// Check if the hint is a valid URL
try {
new URL(hint);
return true;
} catch (e) {
return false;
}
}

function isRelayHintConnected(ndk: NDK, hint: string | undefined) {
if (!isValidHint(hint)) return false;

return ndk.pool.isRelayConnected(hint!);
}

/**
* @ignore
*/
export async function fetchEventFromTag(
this: NDK,
tag: NDKTag,
originalEvent: NDKEvent,
subOpts?: NDKSubscriptionOptions,
fallback: NDKFetchFallbackOptions = {
type: "timeout",
}
) {
const d = this.debug.extend("fetch-event-from-tag");
const [tagType, id, hint] = tag;
const [_, id, hint] = tag;

// If we are supposed to stick to the cache, just go with that
// if (subOpts?.cacheUsage === NDKSubscriptionCacheUsage.ONLY_CACHE) {
// return this.fetchEvent(id, subOpts);
// }

// XXXXX
subOpts = {};
if (!isValidHint(hint)) return;

console;
d("fetching event from tag", tag, subOpts, fallback);

// If we are connected to the relay hint, try exclusively from that relay
// if (isRelayHintConnected(this, hint)) {
// d("fetching event from connected relay hint (%s)", normalizeRelayUrl(hint));
// let event = await this.fetchEvent(id, subOpts, this.pool.getRelay(hint));
// if (event) return event;
// }

// Check if we have a relay list for the author of the original event
// and prefer to use those relays
const authorRelays = getWriteRelaysFor(this, originalEvent.pubkey);
if (authorRelays && authorRelays.size > 0) {
d("fetching event from author relays %o", Array.from(authorRelays));
const relaySet = NDKRelaySet.fromRelayUrls(Array.from(authorRelays), this);
let event = await this.fetchEvent(id, subOpts, relaySet);
if (event) return event;
} else {
d("no author relays found for %s", originalEvent.pubkey, originalEvent);
}

// Attempt without relay hint on whatever NDK calculates
const relaySet = calculateRelaySetsFromFilters(this, [{ ids: [id] }], this.pool);
d("fetching event without relay hint", relaySet);
let event = await this.fetchEvent(id, subOpts);
if (event) return event;

// If we didn't get the event, try to fetch in the relay hint
if (hint && hint !== "") {
let event = await this.fetchEvent(
id,
subOpts,
this.pool.getRelay(hint, true, true, [{ ids: [id] }])
);
if (event) return event;
}

let result: NDKEvent | null | undefined = undefined;

let relay =
hint && hint !== "" ? this.pool.getRelay(hint, true, true, [{ ids: [id] }]) : undefined;
let relay = isValidHint(hint)
? this.pool.getRelay(hint, false, true, [{ ids: [id] }])
: undefined;

/**
* Fetch with (maybe) a relay hint.
*/
const fetchMaybeWithRelayHint = new Promise<NDKEvent | null>((resolve) => {
this.fetchEvent(id, subOpts, relay).then(resolve);
});

// if we don't have a relay hint we don't need to setup a fallback
if (hint === "" || !hint || fallback.type === "none") {
if (!isValidHint(hint) || fallback.type === "none") {
return fetchMaybeWithRelayHint;
}

Expand Down
19 changes: 8 additions & 11 deletions ndk/src/ndk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export interface GetUserParams extends NDKUserParams {
hexpubkey?: string;
}

export const DEFAULT_OUTBOX_RELAYS = ["wss://purplepag.es/", "wss://profiles.nos.social/"];
export const DEFAULT_OUTBOX_RELAYS = ["wss://purplepag.es/", "wss://nos.lol/"];

/**
* TODO: Move this to a outbox policy
Expand Down Expand Up @@ -491,9 +491,12 @@ export class NDK extends EventEmitter<{
}

/**
* Fetches event following a tag
* @param tag
* @param subOpts
* Attempts to fetch an event from a tag, following relay hints and
* other best practices.
* @param tag Tag to fetch the event from
* @param originalEvent Event where the tag came from
* @param subOpts Subscription options to use when fetching the event
* @param fallback Fallback options to use when the hint relay doesn't respond
* @returns
*/
public fetchEventFromTag = fetchEventFromTag.bind(this);
Expand Down Expand Up @@ -701,12 +704,6 @@ export class NDK extends EventEmitter<{

const zap = new NDKZap(zapOpts);

return zap.createZapRequest(
amount,
comment,
extraTags,
undefined,
signer
);
return zap.createZapRequest(amount, comment, extraTags, undefined, signer);
}
}
8 changes: 4 additions & 4 deletions ndk/src/outbox/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class OutboxTracker extends EventEmitter {
});
}

public trackUsers(items: NDKUser[] | Hexpubkey[]) {
public trackUsers(items: NDKUser[] | Hexpubkey[], skipCache = false) {
for (let i = 0; i < items.length; i += 400) {
const slice = items.slice(i, i + 400);
let pubkeys = slice
Expand All @@ -80,7 +80,7 @@ export class OutboxTracker extends EventEmitter {
this.data.set(pubkey, new OutboxItem("user"));
}

getRelayListForUsers(pubkeys, this.ndk).then(
getRelayListForUsers(pubkeys, this.ndk, skipCache).then(
(relayLists: Map<Hexpubkey, NDKRelayList>) => {
for (const [pubkey, relayList] of relayLists) {
const outboxItem = this.data.get(pubkey)!;
Expand Down Expand Up @@ -112,7 +112,7 @@ export class OutboxTracker extends EventEmitter {
this.data.set(pubkey, outboxItem);

// this.debug(
// `Adding ${outboxItem.readRelays.size} read relays and ${outboxItem.writeRelays.size} write relays for ${user.pubkey}`
// `Adding ${outboxItem.readRelays.size} read relays and ${outboxItem.writeRelays.size} write relays for ${pubkey}, %o`, relayList?.rawEvent()
// );
}
}
Expand All @@ -126,7 +126,7 @@ export class OutboxTracker extends EventEmitter {
* @param key
* @param score
*/
public track(item: NDKUser | Hexpubkey, type?: OutboxItemType): OutboxItem {
public track(item: NDKUser | Hexpubkey, type?: OutboxItemType, skipCache = true): OutboxItem {
const key = getKeyFromItem(item);
type ??= getTypeFromItem(item);
let outboxItem = this.data.get(key);
Expand Down
24 changes: 14 additions & 10 deletions ndk/src/relay/connectivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class NDKRelayConnectivity {

const authHandler = async (challenge: string) => {
const authPolicy = this.ndkRelay.authPolicy ?? this.ndk?.relayAuthDefaultPolicy;

this.debug("Relay requested authentication", {
havePolicy: !!authPolicy,
});
Expand All @@ -79,15 +79,19 @@ export class NDKRelayConnectivity {
});
}

if (this._status === NDKRelayStatus.AUTHENTICATING) {
this.debug("Authentication policy finished");
this.relay.auth(async (evt: EventTemplate): Promise<VerifiedEvent> => {
const event = new NDKEvent(this.ndk, evt as NostrEvent);
await event.sign();
return event.rawEvent() as VerifiedEvent;
});
this._status = NDKRelayStatus.CONNECTED;
this.ndkRelay.emit("authed");
if (res === true) {
if (!this.ndk?.signer) {
throw new Error("No signer available for authentication");
} else if (this._status === NDKRelayStatus.AUTHENTICATING) {
this.debug("Authentication policy finished");
this.relay.auth(async (evt: EventTemplate): Promise<VerifiedEvent> => {
const event = new NDKEvent(this.ndk, evt as NostrEvent);
await event.sign();
return event.rawEvent() as VerifiedEvent;
});
this._status = NDKRelayStatus.CONNECTED;
this.ndkRelay.emit("authed");
}
}
}
} else {
Expand Down
6 changes: 1 addition & 5 deletions ndk/src/relay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,7 @@ export class NDKRelay extends EventEmitter<{
public complaining = false;
readonly debug: debug.Debugger;

public constructor(
url: WebSocket["url"],
authPolicy?: NDKAuthPolicy,
ndk?: NDK,
) {
public constructor(url: WebSocket["url"], authPolicy?: NDKAuthPolicy, ndk?: NDK) {
super();
this.url = normalizeRelayUrl(url);
this.scores = new Map<NDKUser, NDKRelayScore>();
Expand Down
Loading

0 comments on commit e353c1c

Please sign in to comment.