import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; import { unwrap } from "@snort/shared"; import { EventKind, EventSigner, FullRelaySettings, HexKey, NostrEvent, NostrLink, NotSignedNostrEvent, PowMiner, PrivateKeySigner, RelaySettings, settingsToRelayTag, SignerSupports, TaggedNostrEvent, ToNostrEventTag, u256, UserMetadata, } from "."; import { EventBuilder } from "./event-builder"; import { findTag } from "./utils"; import { Nip7Signer } from "./impl/nip7"; import { Nip10 } from "./impl/nip10"; type EventBuilderHook = (ev: EventBuilder) => EventBuilder; export class EventPublisher { #pubKey: string; #signer: EventSigner; #pow?: number; #miner?: PowMiner; constructor(signer: EventSigner, pubKey: string) { this.#signer = signer; this.#pubKey = pubKey; } get signer() { return this.#signer; } /** * Create a NIP-07 EventPublisher */ static async nip7() { if ("nostr" in window) { const signer = new Nip7Signer(); const pubkey = await signer.getPubKey(); if (pubkey) { return new EventPublisher(signer, pubkey); } } } /** * Create an EventPublisher for a private key */ static privateKey(privateKey: string) { const signer = new PrivateKeySigner(privateKey); return new EventPublisher(signer, signer.getPubKey()); } supports(t: SignerSupports) { return this.#signer.supports.includes(t); } get pubKey() { return this.#pubKey; } /** * Create a copy of this publisher with PoW */ pow(target: number, miner?: PowMiner) { const ret = new EventPublisher(this.#signer, this.#pubKey); ret.#pow = target; ret.#miner = miner; return ret; } #eb(k: EventKind) { const eb = new EventBuilder(); return eb.pubKey(this.#pubKey).kind(k); } async #sign(eb: EventBuilder) { return await (this.#pow ? eb.pow(this.#pow, this.#miner) : eb).buildAndSign(this.#signer); } async nip4Encrypt(content: string, otherKey: string) { return await this.#signer.nip4Encrypt(content, otherKey); } async nip4Decrypt(content: string, otherKey: string) { return await this.#signer.nip4Decrypt(content, otherKey); } async nip42Auth(challenge: string, relay: string) { const eb = this.#eb(EventKind.Auth); eb.tag(["relay", relay]); eb.tag(["challenge", challenge]); return await this.#sign(eb); } /** * Build a mute list event using lists of pubkeys * @param pub Public mute list * @param priv Private mute list */ async muted(pub: Array, priv: Array) { const eb = this.#eb(EventKind.MuteList); pub.forEach(p => { eb.tag(["p", p]); }); if (priv.length > 0) { const ps = priv.map(p => ["p", p]); const plaintext = JSON.stringify(ps); eb.content(await this.nip4Encrypt(plaintext, this.#pubKey)); } return await this.#sign(eb); } /** * Build a pin list event using lists of event links */ async pinned(notes: Array) { const eb = this.#eb(EventKind.PinList); notes.forEach(n => { eb.tag(unwrap(n.toEventTag())); }); return await this.#sign(eb); } /** * Build a categorized bookmarks event with a given label * @param notes List of bookmarked links */ async bookmarks(notes: Array) { const eb = this.#eb(EventKind.BookmarksList); notes.forEach(n => { eb.tag(unwrap(n.toEventTag())); }); return await this.#sign(eb); } async metadata(obj: UserMetadata) { const eb = this.#eb(EventKind.SetMetadata); eb.content(JSON.stringify(obj)); return await this.#sign(eb); } /** * Create a basic text note */ async note(msg: string, fnExtra?: EventBuilderHook) { const eb = this.#eb(EventKind.TextNote); eb.content(msg); eb.processContent(); fnExtra?.(eb); return await this.#sign(eb); } /** * Create a zap request event for a given target event/profile * @param amount Millisats amout! * @param author Author pubkey to tag in the zap * @param note Note Id to tag in the zap * @param msg Custom message to be included in the zap */ async zap( amount: number, author: HexKey, relays: Array, note?: NostrLink, msg?: string, fnExtra?: EventBuilderHook, ) { const eb = this.#eb(EventKind.ZapRequest); eb.content(msg?.trim() ?? ""); if (note) { // HACK: remove relay tag, some zap services dont like relay tags eb.tag(unwrap(note.toEventTag()).slice(0, 2)); } eb.tag(["p", author]); eb.tag(["relays", ...relays.map(a => a.trim())]); eb.tag(["amount", amount.toString()]); eb.processContent(); fnExtra?.(eb); return await this.#sign(eb); } /** * Reply to a note */ async reply(replyTo: TaggedNostrEvent, msg: string, fnExtra?: EventBuilderHook) { const eb = this.#eb(EventKind.TextNote); eb.content(msg); Nip10.replyTo(replyTo, eb); eb.processContent(); fnExtra?.(eb); return await this.#sign(eb); } async react(evRef: NostrEvent, content = "+") { const eb = this.#eb(EventKind.Reaction); eb.content(content); eb.tag(unwrap(NostrLink.fromEvent(evRef).toEventTag())); eb.tag(["p", evRef.pubkey]); return await this.#sign(eb); } async relayList(relays: Array | Record) { if (!Array.isArray(relays)) { relays = Object.entries(relays).map(([k, v]) => ({ url: k, settings: v, })); } const eb = this.#eb(EventKind.Relays); for (const rx of relays) { const tag = settingsToRelayTag(rx); if (tag) { eb.tag(tag); } } return await this.#sign(eb); } async contactList(tags: Array<[string, string]>, relays?: Record) { const eb = this.#eb(EventKind.ContactList); tags.forEach(a => eb.tag(a)); if (relays) { eb.content(JSON.stringify(relays)); } return await this.#sign(eb); } /** * Delete an event (NIP-09) */ async delete(id: u256) { const eb = this.#eb(EventKind.Deletion); eb.tag(["e", id]); return await this.#sign(eb); } /** * Repost a note (NIP-18) */ async repost(note: NostrEvent) { const eb = this.#eb(EventKind.Repost); eb.tag(unwrap(NostrLink.fromEvent(note).toEventTag())); eb.tag(["p", note.pubkey]); return await this.#sign(eb); } async decryptDm(note: NostrEvent) { if (note.kind === EventKind.SealedRumor) { const unseal = await this.unsealRumor(note); return unseal.content; } if ( note.kind === EventKind.DirectMessage && note.pubkey !== this.#pubKey && !note.tags.some(a => a[1] === this.#pubKey) ) { throw new Error("Can't decrypt, DM does not belong to this user"); } const otherPubKey = note.pubkey === this.#pubKey ? unwrap(note.tags.find(a => a[0] === "p")?.[1]) : note.pubkey; return await this.nip4Decrypt(note.content, otherPubKey); } async sendDm(content: string, to: HexKey) { const eb = this.#eb(EventKind.DirectMessage); eb.content(await this.nip4Encrypt(content, to)); eb.tag(["p", to]); return await this.#sign(eb); } async generic(fnHook: EventBuilderHook) { const eb = new EventBuilder(); eb.pubKey(this.#pubKey); fnHook(eb); return await this.#sign(eb); } async appData(data: object, id: string) { const eb = this.#eb(EventKind.AppData); eb.content(await this.nip4Encrypt(JSON.stringify(data), this.#pubKey)); eb.tag(["d", id]); return await this.#sign(eb); } /** * NIP-59 Gift Wrap event with ephemeral key */ async giftWrap(inner: NostrEvent, explicitP?: string, powTarget?: number, powMiner?: PowMiner) { const secret = utils.bytesToHex(secp.secp256k1.utils.randomPrivateKey()); const signer = new PrivateKeySigner(secret); const pTag = explicitP ?? findTag(inner, "p"); if (!pTag) throw new Error("Inner event must have a p tag"); const eb = new EventBuilder(); eb.pubKey(signer.getPubKey()); eb.kind(EventKind.GiftWrap); eb.tag(["p", pTag]); if (powTarget) { eb.pow(powTarget, powMiner); } eb.content(await signer.nip44Encrypt(JSON.stringify(inner), pTag)); return await eb.buildAndSign(secret); } async unwrapGift(gift: NostrEvent) { const body = await this.#signer.nip44Decrypt(gift.content, gift.pubkey); return JSON.parse(body) as NostrEvent; } /** * Create an unsigned gossip message */ createUnsigned(kind: EventKind, content: string, fnHook: EventBuilderHook) { const eb = new EventBuilder(); eb.pubKey(this.pubKey); eb.kind(kind); eb.content(content); fnHook(eb); return eb.build() as NotSignedNostrEvent; } /** * Create sealed rumor */ async sealRumor(inner: NotSignedNostrEvent, toKey: string) { const eb = this.#eb(EventKind.SealedRumor); eb.content(await this.#signer.nip44Encrypt(JSON.stringify(inner), toKey)); return await this.#sign(eb); } /** * Unseal rumor */ async unsealRumor(inner: NostrEvent) { if (inner.kind !== EventKind.SealedRumor) throw new Error("Not a sealed rumor event"); const body = await this.#signer.nip44Decrypt(inner.content, inner.pubkey); return JSON.parse(body) as NostrEvent; } }