UNPKG

@nostr-dev-kit/ndk

Version:

NDK - Nostr Development Kit

1,087 lines (951 loc) 36.3 kB
import { EventEmitter } from "tseep"; import type { NDK } from "../ndk/index.js"; import type { NDKRelay } from "../relay/index.js"; import { calculateRelaySetFromEvent } from "../relay/sets/calculate.js"; import type { NDKRelaySet } from "../relay/sets/index.js"; import type { NDKSigner } from "../signers/index.js"; import type { NDKFilter } from "../subscription/index.js"; import type { NDKUser } from "../user/index.js"; import { type ContentTag, generateContentTags, mergeTags } from "./content-tagger.js"; import { decrypt, encrypt } from "./encryption.js"; import { fetchReplyEvent, fetchRootEvent, fetchTaggedEvent } from "./fetch-tagged-event.js"; import { isEphemeral, isParamReplaceable, isReplaceable } from "./kind.js"; import { NDKKind } from "./kinds/index.js"; import { encode } from "./nip19.js"; import type { NIP73EntityType } from "./nip73.js"; import { repost } from "./repost.js"; import { type NDKEventSerialized, deserialize, serialize } from "./serializer.js"; import { getEventHash, validate, verifySignature } from "./validation.js"; const skipClientTagOnKinds = new Set([ NDKKind.Metadata, NDKKind.EncryptedDirectMessage, NDKKind.GiftWrap, NDKKind.GiftWrapSeal, NDKKind.Contacts, NDKKind.ZapRequest, NDKKind.EventDeletion, ]); export type NDKEventId = string; export type NDKTag = string[]; export type NostrEvent = { created_at: number; content: string; tags: NDKTag[]; kind?: NDKKind | number; pubkey: string; id?: string; sig?: string; }; /** * A finalized event */ export type NDKRawEvent = { created_at: number; content: string; tags: NDKTag[]; kind: NDKKind | number; pubkey: string; id: string; sig: string; }; /** * NDKEvent is the basic building block of NDK; most things * you do with NDK will revolve around writing or consuming NDKEvents. */ export class NDKEvent extends EventEmitter { public ndk?: NDK; public created_at?: number; public content = ""; public tags: NDKTag[] = []; public kind: NDKKind | number; public id = ""; public sig?: string; public pubkey = ""; public signatureVerified?: boolean; private _author: NDKUser | undefined = undefined; /** * The relay that this event was first received from. */ public relay: NDKRelay | undefined; /** * The relays that this event was received from and/or successfully published to. */ get onRelays(): NDKRelay[] { let res: NDKRelay[] = []; if (!this.ndk) { if (this.relay) res.push(this.relay); } else { res = this.ndk.subManager.seenEvents.get(this.id) || []; } return res; } /** * The status of the publish operation. */ public publishStatus?: "pending" | "success" | "error" = "success"; public publishError?: Error; constructor(ndk?: NDK, event?: Partial<NDKRawEvent> | NDKEvent) { 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!; if (event instanceof NDKEvent) { if (this.relay) { this.relay = event.relay; this.ndk?.subManager.seenEvent(event.id, this.relay!); } this.publishStatus = event.publishStatus; this.publishError = event.publishError; } } /** * Deserialize an NDKEvent from a serialized payload. * @param ndk * @param event * @returns */ static deserialize(ndk: NDK | undefined, event: NDKEventSerialized): NDKEvent { return new NDKEvent(ndk, deserialize(event)); } /** * Returns the event as is. */ public rawEvent(): NDKRawEvent { 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: NDKUser) { this.pubkey = user.pubkey; this._author = user; this._author.ndk ??= this.ndk; } /** * Returns an NDKUser for the author of the event. */ get author(): NDKUser { if (this._author) return this._author; if (!this.ndk) throw new Error("No NDK instance found"); const user = this.ndk.getUser({ pubkey: this.pubkey }); this._author = user; return user; } /** * NIP-73 tagging of external entities * @param entity to be tagged * @param type of the entity * @param markerUrl to be used as the marker URL * * @example * ```typescript * event.tagExternal("https://example.com/article/123#nostr", "url"); * event.tags => [["i", "https://example.com/123"], ["k", "https://example.com"]] * ``` * * @example tag a podcast:item:guid * ```typescript * event.tagExternal("e32b4890-b9ea-4aef-a0bf-54b787833dc5", "podcast:item:guid"); * event.tags => [["i", "podcast:item:guid:e32b4890-b9ea-4aef-a0bf-54b787833dc5"], ["k", "podcast:item:guid"]] * ``` * * @see https://github.com/nostr-protocol/nips/blob/master/73.md */ public tagExternal(entity: string, type: NIP73EntityType, markerUrl?: string) { const iTag: NDKTag = ["i"]; const kTag: NDKTag = ["k"]; switch (type) { case "url": { const url = new URL(entity); url.hash = ""; // Remove the fragment iTag.push(url.toString()); kTag.push(`${url.protocol}//${url.host}`); break; } case "hashtag": iTag.push(`#${entity.toLowerCase()}`); kTag.push("#"); break; case "geohash": iTag.push(`geo:${entity.toLowerCase()}`); kTag.push("geo"); break; case "isbn": iTag.push(`isbn:${entity.replace(/-/g, "")}`); kTag.push("isbn"); break; case "podcast:guid": iTag.push(`podcast:guid:${entity}`); kTag.push("podcast:guid"); break; case "podcast:item:guid": iTag.push(`podcast:item:guid:${entity}`); kTag.push("podcast:item:guid"); break; case "podcast:publisher:guid": iTag.push(`podcast:publisher:guid:${entity}`); kTag.push("podcast:publisher:guid"); break; case "isan": iTag.push(`isan:${entity.split("-").slice(0, 4).join("-")}`); kTag.push("isan"); break; case "doi": iTag.push(`doi:${entity.toLowerCase()}`); kTag.push("doi"); break; default: throw new Error(`Unsupported NIP-73 entity type: ${type}`); } if (markerUrl) { iTag.push(markerUrl); } this.tags.push(iTag); this.tags.push(kTag); } /** * Tag a user with an optional marker. * @param target What is to be tagged. Can be an NDKUser, NDKEvent, or an NDKTag. * @param marker The marker to use in the tag. * @param skipAuthorTag Whether to explicitly skip adding the author tag of the event. * @param forceTag Force a specific tag to be used instead of the default "e" or "a" tag. * @example * ```typescript * reply.tag(opEvent, "reply"); * // reply.tags => [["e", <id>, <relay>, "reply"]] * ``` */ public tag( target: NDKTag | NDKUser | NDKEvent, marker?: string, skipAuthorTag?: boolean, forceTag?: string ): void { let tags: NDKTag[] = []; const isNDKUser = (target as NDKUser).fetchProfile !== undefined; if (isNDKUser) { forceTag ??= "p"; const tag = [forceTag, (target as NDKUser).pubkey]; if (marker) tag.push(...["", marker]); tags.push(tag); } else if (target instanceof NDKEvent) { const event = target as NDKEvent; skipAuthorTag ??= event?.pubkey === this.pubkey; tags = event.referenceTags(marker, skipAuthorTag, forceTag); // tag p-tags in the event if they are not the same as the user signing this event for (const pTag of event.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]]); } } else if (Array.isArray(target)) { tags = [target as NDKTag]; } else { throw new Error("Invalid argument", target as any); } this.tags = mergeTags(this.tags, tags); } /** * 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?: string): Promise<NostrEvent> { if (!pubkey && this.pubkey === "") { const user = await this.ndk?.signer?.user(); this.pubkey = user?.pubkey || ""; } if (!this.created_at) { this.created_at = Math.floor(Date.now() / 1000); } const { content, tags } = await this.generateTags(); this.content = content || ""; this.tags = tags; try { this.id = this.getEventHash(); // eslint-disable-next-line no-empty } catch (_e) {} // if (this.id) nostrEvent.id = this.id; // if (this.sig) nostrEvent.sig = this.sig; return this.rawEvent(); } public serialize = serialize.bind(this); public getEventHash = getEventHash.bind(this); public validate = validate.bind(this); public verifySignature = verifySignature.bind(this); /** * Is this event replaceable (whether parameterized or not)? * * This will return true for kind 0, 3, 10k-20k and 30k-40k */ public isReplaceable = isReplaceable.bind(this); public isEphemeral = isEphemeral.bind(this); public isDvm = () => this.kind && this.kind >= 5000 && this.kind <= 7000; /** * Is this event parameterized replaceable? * * This will return true for kind 30k-40k */ public isParamReplaceable = isParamReplaceable.bind(this); /** * Encodes a bech32 id. * * @param relays {string[]} The relays to encode in the id * @returns {string} - Encoded naddr, note or nevent. */ public encode = encode.bind(this); public encrypt = encrypt.bind(this); public 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 */ public getMatchingTags(tagName: string, marker?: string): NDKTag[] { const t = this.tags.filter((tag) => tag[0] === tagName); if (marker === undefined) return t; return t.filter((tag) => tag[3] === marker); } /** * Check if the event has a tag with the given name * @param tagName * @param marker * @returns */ public hasTag(tagName: string, marker?: string): boolean { return this.tags.some((tag) => tag[0] === tagName && (!marker || tag[3] === marker)); } /** * 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 */ public tagValue(tagName: string, marker?: string): string | undefined { const tags = this.getMatchingTags(tagName, marker); if (tags.length === 0) return undefined; return tags[0][1]; } /** * Gets the NIP-31 "alt" tag of the event. */ get alt(): string | undefined { return this.tagValue("alt"); } /** * Sets the NIP-31 "alt" tag of the event. Use this to set an alt tag so * clients that don't handle a particular event kind can display something * useful for users. */ set alt(alt: string | undefined) { this.removeTag("alt"); if (alt) this.tags.push(["alt", alt]); } /** * Gets the NIP-33 "d" tag of the event. */ get dTag(): string | undefined { return this.tagValue("d"); } /** * Sets the NIP-33 "d" tag of the event. */ set dTag(value: string | undefined) { this.removeTag("d"); if (value) this.tags.push(["d", value]); } /** * Remove all tags with the given name (e.g. "d", "a", "p") * @param tagName Tag name(s) to search for and remove * @param marker Optional marker to check for too * * @example * Remove a tags with a "defer" marker * ```typescript * event.tags = [ * ["a", "....", "defer"], * ["a", "....", "no-defer"], * ] * * event.removeTag("a", "defer"); * * // event.tags => [["a", "....", "no-defer"]] * * @returns {void} */ public removeTag(tagName: string | string[], marker?: string): void { const tagNames = Array.isArray(tagName) ? tagName : [tagName]; this.tags = this.tags.filter((tag) => { const include = tagNames.includes(tag[0]); const hasMarker = marker ? tag[3] === marker : true; return !(include && hasMarker); }); } /** * Replace a tag with a new value. If not found, it will be added. * @param tag The tag to replace. * @param value The new value for the tag. */ public replaceTag(tag: NDKTag): void { this.removeTag(tag[0]); this.tags.push(tag); } /** * 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. */ public async sign(signer?: NDKSigner): Promise<string> { if (!signer) { this.ndk?.assertSigner(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion signer = this.ndk?.signer!; } else { this.author = await signer.user(); } const nostrEvent = await this.toNostrEvent(); this.sig = await signer.sign(nostrEvent); return this.sig; } /** * * @param relaySet * @param timeoutMs * @param requiredRelayCount * @returns */ public async publishReplaceable( relaySet?: NDKRelaySet, timeoutMs?: number, requiredRelayCount?: number ) { this.id = ""; this.created_at = Math.floor(Date.now() / 1000); this.sig = ""; return this.publish(relaySet, timeoutMs, requiredRelayCount); } /** * 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. * @param timeoutM {number} The timeout for the publish operation in milliseconds. * @param requiredRelayCount The number of relays that must receive the event for the publish to be considered successful. * @returns A promise that resolves to the relays the event was published to. */ public async publish( relaySet?: NDKRelaySet, timeoutMs?: number, requiredRelayCount?: number ): Promise<Set<NDKRelay>> { if (!requiredRelayCount) requiredRelayCount = 1; 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.size === 0) { // If we have a devWriteRelaySet, use it to publish all events relaySet = this.ndk.devWriteRelaySet || (await calculateRelaySetFromEvent(this.ndk, this, requiredRelayCount)); } // If the published event is a delete event, notify the cache if there is one if (this.kind === NDKKind.EventDeletion && this.ndk.cacheAdapter?.deleteEventIds) { const eTags = this.getMatchingTags("e").map((tag) => tag[1]); this.ndk.cacheAdapter.deleteEventIds(eTags); } const rawEvent = this.rawEvent(); // add to cache for optimistic updates if (this.ndk.cacheAdapter?.addUnpublishedEvent && shouldTrackUnpublishedEvent(this)) { try { this.ndk.cacheAdapter.addUnpublishedEvent(this, relaySet.relayUrls); } catch (e) { console.error("Error adding unpublished event to cache", e); } } // if this is a delete event, send immediately to the cache if (this.kind === NDKKind.EventDeletion && this.ndk.cacheAdapter?.deleteEventIds) { this.ndk.cacheAdapter.deleteEventIds(this.getMatchingTags("e").map((tag) => tag[1])); } // send to active subscriptions that want this event this.ndk.subManager.dispatchEvent(rawEvent, undefined, true); const relays = await relaySet.publish(this, timeoutMs, requiredRelayCount); relays.forEach((relay) => this.ndk?.subManager.seenEvent(this.id, relay)); return relays; } /** * 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(): Promise<ContentTag> { let tags: NDKTag[] = []; // don't autogenerate if there currently are tags const g = await generateContentTags(this.content, this.tags); const content = g.content; tags = g.tags; // if this is a parameterized replaceable event, check if there's a d tag, if not, generate it if (this.kind && this.isParamReplaceable()) { const dTag = this.getMatchingTags("d")[0]; // generate a string of 16 random bytes if (!dTag) { const title = this.tagValue("title"); const randLength = title ? 6 : 16; let str = [...Array(randLength)].map(() => Math.random().toString(36)[2]).join(""); if (title && title.length > 0) { str = `${title.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "")}-${str}`; } tags.push(["d", str]); } } if (this.shouldAddClientTag) { const clientTag: NDKTag = ["client", this.ndk?.clientName ?? ""]; if (this.ndk?.clientNip89) clientTag.push(this.ndk?.clientNip89); tags.push(clientTag); } else if (this.shouldStripClientTag) { tags = tags.filter((tag) => tag[0] !== "client"); } return { content: content || "", tags }; } get shouldAddClientTag(): boolean { if (!this.ndk?.clientName && !this.ndk?.clientNip89) return false; if (skipClientTagOnKinds.has(this.kind!)) return false; if (this.isEphemeral()) return false; if (this.isReplaceable() && !this.isParamReplaceable()) return false; if (this.isDvm()) return false; if (this.hasTag("client")) return false; return true; } get shouldStripClientTag(): boolean { return skipClientTagOnKinds.has(this.kind!); } public muted(): string | null { 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. * * @deprecated Use `dTag` instead. */ replaceableDTag() { if (this.kind && this.kind >= 30000 && this.kind <= 40000) { 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(): string { if ( this.kind === 0 || this.kind === 3 || (this.kind && this.kind >= 10000 && this.kind < 20000) ) { return `${this.kind}:${this.pubkey}`; } 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(): string { // NIP-33 if (this.isParamReplaceable()) { return this.tagAddress(); } return this.id; } /** * Returns a stable reference value for a replaceable event. * * Param replaceable events are returned in the expected format of `<kind>:<pubkey>:<d-tag>`. * Kind-replaceable events are returned in the format of `<kind>:<pubkey>:`. * * @returns {string} A stable reference value for replaceable events */ tagAddress(): string { if (this.isParamReplaceable()) { const dTagId = this.dTag ?? ""; return `${this.kind}:${this.pubkey}:${dTagId}`; } if (this.isReplaceable()) { return `${this.kind}:${this.pubkey}:`; } throw new Error("Event is not a replaceable event"); } /** * Determines the type of tag that can be used to reference this event from another event. * @returns {string} The tag type * @example * event = new NDKEvent(ndk, { kind: 30000, pubkey: 'pubkey', tags: [ ["d", "d-code"] ] }); * event.tagType(); // "a" */ tagType(): "e" | "a" { return this.isParamReplaceable() ? "a" : "e"; } /** * Get the tag that can be used to reference this event from another event. * * Consider using referenceTags() instead (unless you have a good reason to use this) * * @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(marker?: string): NDKTag { let tag: NDKTag; // NIP-33 if (this.isParamReplaceable()) { tag = ["a", this.tagAddress()]; } else { tag = ["e", this.tagId()]; } if (this.relay) { tag.push(this.relay.url); } else { tag.push(""); } tag.push(marker ?? ""); if (!this.isParamReplaceable()) { tag.push(this.pubkey); } return tag; } /** * Get the tags that can be used to reference this event from another event * @param marker The marker to use in the tag * @param skipAuthorTag Whether to explicitly skip adding the author tag of the event * @param forceTag Force a specific tag to be used instead of the default "e" or "a" 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?: string, skipAuthorTag?: boolean, forceTag?: string): NDKTag[] { let tags: NDKTag[] = []; // NIP-33 if (this.isParamReplaceable()) { tags = [ [forceTag ?? "a", this.tagAddress()], [forceTag ?? "e", this.id], ]; } else { tags = [[forceTag ?? "e", this.id]]; } // Add the relay url to all tags tags = tags.map((tag) => { if (tag[0] === "e" || marker) { tag.push(this.relay?.url ?? ""); } else if (this.relay?.url) { tag.push(this.relay?.url); } return tag; }); // add marker and pubkey to e tags, and marker to a tags tags.forEach((tag) => { if (tag[0] === "e") { tag.push(marker ?? ""); tag.push(this.pubkey); } else if (marker) { tag.push(marker); } }); // NIP-29 h-tags tags = [...tags, ...this.getMatchingTags("h")]; if (!skipAuthorTag) 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(): NDKFilter { if (this.isParamReplaceable()) { return { "#a": [this.tagId()] }; } return { "#e": [this.tagId()] }; } nip22Filter(): NDKFilter { if (this.isParamReplaceable()) { return { "#A": [this.tagId()] }; } return { "#E": [this.tagId()] }; } /** * Generates a deletion event of the current event * * @param reason The reason for the deletion * @param publish Whether to publish the deletion event automatically * @returns The deletion event */ async delete(reason?: string, publish = true): Promise<NDKEvent> { if (!this.ndk) throw new Error("No NDK instance found"); this.ndk.assertSigner(); const e = new NDKEvent(this.ndk, { kind: NDKKind.EventDeletion, content: reason || "", } as NostrEvent); e.tag(this, undefined, true); e.tags.push(["k", this.kind?.toString()]); if (publish) { this.emit("deleted"); await e.publish(); } return e; } /** * Establishes whether this is a NIP-70-protectede event. * @@satisfies NIP-70 */ set isProtected(val: boolean) { this.removeTag("-"); if (val) this.tags.push(["-"]); } /** * Whether this is a NIP-70-protected event. * @@satisfies NIP-70 */ get isProtected(): boolean { return this.hasTag("-"); } /** * Fetch an event tagged with the given tag following relay hints if provided. * @param tag The tag to search for * @param marker The marker to use in the tag (e.g. "root") * @returns The fetched event or null if no event was found, undefined if no matching tag was found in the event * * @example * const replyEvent = await ndk.fetchEvent("nevent1qqs8x8vnycyha73grv380gmvlury4wtmx0nr9a5ds2dngqwgu87wn6gpzemhxue69uhhyetvv9ujuurjd9kkzmpwdejhgq3ql2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqz4cwjd") * const originalEvent = await replyEvent.fetchTaggedEvent("e", "reply"); * console.log(replyEvent.encode() + " is a reply to event " + originalEvent?.encode()); */ public fetchTaggedEvent = fetchTaggedEvent.bind(this); /** * Fetch the root event of the current event. * @returns The fetched root event or null if no event was found * @example * const replyEvent = await ndk.fetchEvent("nevent1qqs8x8vnycyha73grv380gmvlury4wtmx0nr9a5ds2dngqwgu87wn6gpzemhxue69uhhyetvv9ujuurjd9kkzmpwdejhgq3ql2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqz4cwjd") * const rootEvent = await replyEvent.fetchRootEvent(); * console.log(replyEvent.encode() + " is a reply in the thread " + rootEvent?.encode()); */ public fetchRootEvent = fetchRootEvent.bind(this); /** * Fetch the event the current event is replying to. * @returns The fetched reply event or null if no event was found */ public fetchReplyEvent = fetchReplyEvent.bind(this); /** * NIP-18 reposting event. * * @param publish Whether to publish the reposted event automatically @default true * @param signer The signer to use for signing the reposted event * @returns The reposted event * * @function */ public repost = repost.bind(this); /** * React to an existing event * * @param content The content of the reaction */ async react(content: string, publish = true): Promise<NDKEvent> { if (!this.ndk) throw new Error("No NDK instance found"); this.ndk.assertSigner(); const e = new NDKEvent(this.ndk, { kind: NDKKind.Reaction, content, } as NostrEvent); e.tag(this); // add a [ "k", kind ] for all non-kind:1 events if (this.kind !== NDKKind.Text) { e.tags.push(["k", `${this.kind}`]); } if (publish) await e.publish(); return e; } /** * Checks whether the event is valid per underlying NIPs. * * This method is meant to be overridden by subclasses that implement specific NIPs * to allow the enforcement of NIP-specific validation rules. * * Otherwise, it will only check for basic event properties. * */ get isValid(): boolean { return this.validate(); } get inspect(): string { return JSON.stringify(this.rawEvent(), null, 4); } /** * Dump the event to console for debugging purposes. * Prints a JSON stringified version of rawEvent() with indentation * and also lists all relay URLs for onRelays. */ public dump(): void { console.debug(JSON.stringify(this.rawEvent(), null, 4)); console.debug("Event on relays:", this.onRelays.map((relay) => relay.url).join(", ")); } /** * Creates a reply event for the current event. * * This function will use NIP-22 when appropriate (i.e. replies to non-kind:1 events). * This function does not have side-effects; it will just return an event with the appropriate tags * to generate the reply event; the caller is responsible for publishing the event. * * @param forceNip22 - Optional flag to force NIP-22 style replies (kind 1111) regardless of the original event's kind */ public reply(forceNip22?: boolean): NDKEvent { const reply = new NDKEvent(this.ndk); if (this.kind === 1 && !forceNip22) { reply.kind = 1; const opHasETag = this.hasTag("e"); if (opHasETag) { reply.tags = [ ...reply.tags, ...this.getMatchingTags("e"), ...this.getMatchingTags("p"), ...this.getMatchingTags("a"), ...this.referenceTags("reply"), ]; } else { reply.tag(this, "root"); } } else { reply.kind = NDKKind.GenericReply; const carryOverTags = ["A", "E", "I", "P"]; const rootTags = this.tags.filter((tag) => carryOverTags.includes(tag[0])); // we have a root tag already if (rootTags.length > 0) { const rootKind = this.tagValue("K"); reply.tags.push(...rootTags); if (rootKind) reply.tags.push(["K", rootKind]); const [type, id, _, ...extra] = this.tagReference(); const tag = [type, id, ...extra]; reply.tags.push(tag); } else { const [type, id, _, relayHint] = this.tagReference(); const tag = [type, id, relayHint ?? ""]; if (type === "e") tag.push(this.pubkey); reply.tags.push(tag); const uppercaseTag = [...tag]; uppercaseTag[0] = uppercaseTag[0].toUpperCase(); reply.tags.push(uppercaseTag); reply.tags.push(["K", this.kind?.toString()]); reply.tags.push(["P", this.pubkey]); } reply.tags.push(["k", this.kind?.toString()]); // carry over all p tags reply.tags.push(...this.getMatchingTags("p")); reply.tags.push(["p", this.pubkey]); } return reply; } } const untrackedUnpublishedEvents = new Set([ NDKKind.NostrConnect, NDKKind.NostrWaletConnectInfo, NDKKind.NostrWalletConnectReq, NDKKind.NostrWalletConnectRes, ]); function shouldTrackUnpublishedEvent(event: NDKEvent): boolean { return !untrackedUnpublishedEvents.has(event.kind!); } /** * Discriminated union types for signed and unsigned events */ /** * An NDKEvent that has been signed and has all required fields for relay transmission */ export type NDKSignedEvent = NDKEvent & { readonly signed: true; id: string; // narrows string to required sig: string; // narrows string | undefined to required string created_at: number; // narrows number | undefined to required number }; /** * An NDKEvent that has not been signed yet */ export type NDKUnsignedEvent = NDKEvent & { readonly signed: false; id?: string; sig?: string; created_at?: number; }; /** * Union type representing either signed or unsigned NDKEvent */ export type NDKEventVariant = NDKSignedEvent | NDKUnsignedEvent; /** * Type guard to check if an event is signed */ export function isSignedEvent(event: NDKEvent): event is NDKSignedEvent { return !!(event.sig && event.id && event.created_at && event.created_at > 0); } /** * Type guard to check if an event is unsigned */ export function isUnsignedEvent(event: NDKEvent): event is NDKUnsignedEvent { return !isSignedEvent(event); } /** * Assertion function for when you know an event must be signed */ export function assertSignedEvent(event: NDKEvent): asserts event is NDKSignedEvent { if (!isSignedEvent(event)) { throw new Error("Expected signed event but event is not signed"); } } /** * Factory function to create a typed signed event (used internally by subscriptions) */ export function createSignedEvent(event: NDKEvent): NDKSignedEvent { if (!isSignedEvent(event)) { throw new Error("Cannot create signed event from unsigned event"); } // TypeScript now knows this is NDKSignedEvent Object.defineProperty(event, "signed", { value: true, writable: false, enumerable: false }); return event as NDKSignedEvent; }