dfklfg/packages/system/src/event-publisher.ts
2023-09-05 14:57:50 +01:00

357 lines
9.3 KiB
TypeScript

import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils";
import { unwrap, getPublicKey, unixNow } from "@snort/shared";
import {
EventKind,
EventSigner,
FullRelaySettings,
HexKey,
Lists,
NostrEvent,
NotSignedNostrEvent,
PowMiner,
PrivateKeySigner,
RelaySettings,
SignerSupports,
TaggedNostrEvent,
u256,
UserMetadata,
} from ".";
import { EventBuilder } from "./event-builder";
import { EventExt } from "./event-ext";
import { findTag } from "./utils";
import { Nip7Signer } from "./impl/nip7";
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;
}
/**
* 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;
}
/**
* Apply POW to every event
*/
pow(target: number, miner?: PowMiner) {
this.#pow = target;
this.#miner = miner;
}
#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);
}
async muted(keys: HexKey[], priv: HexKey[]) {
const eb = this.#eb(EventKind.PubkeyLists);
eb.tag(["d", Lists.Muted]);
keys.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);
}
async noteList(notes: u256[], list: Lists) {
const eb = this.#eb(EventKind.NoteLists);
eb.tag(["d", list]);
notes.forEach(n => {
eb.tag(["e", n]);
});
return await this.#sign(eb);
}
async tags(tags: string[]) {
const eb = this.#eb(EventKind.TagLists);
eb.tag(["d", Lists.Followed]);
tags.forEach(t => {
eb.tag(["t", t]);
});
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<string>,
note?: HexKey,
msg?: string,
fnExtra?: EventBuilderHook,
) {
const eb = this.#eb(EventKind.ZapRequest);
eb.content(msg ?? "");
if (note) {
eb.tag(["e", note]);
}
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);
const thread = EventExt.extractThread(replyTo);
if (thread) {
if (thread.root || thread.replyTo) {
eb.tag(["e", thread.root?.value ?? thread.replyTo?.value ?? "", "", "root"]);
}
eb.tag(["e", replyTo.id, replyTo.relays?.[0] ?? "", "reply"]);
eb.tag(["p", replyTo.pubkey]);
for (const pk of thread.pubKeys) {
if (pk === this.#pubKey) {
continue;
}
eb.tag(["p", pk]);
}
} else {
eb.tag(["e", replyTo.id, "", "reply"]);
// dont tag self in replies
if (replyTo.pubkey !== this.#pubKey) {
eb.tag(["p", replyTo.pubkey]);
}
}
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(["e", evRef.id]);
eb.tag(["p", evRef.pubkey]);
return await this.#sign(eb);
}
async relayList(relays: Array<FullRelaySettings> | Record<string, RelaySettings>) {
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 rTag = ["r", rx.url];
if (rx.settings.read && !rx.settings.write) {
rTag.push("read");
}
if (rx.settings.write && !rx.settings.read) {
rTag.push("write");
}
if (rx.settings.read || rx.settings.write) {
eb.tag(rTag);
}
}
return await this.#sign(eb);
}
async contactList(follows: Array<HexKey>, relays: Record<string, RelaySettings>) {
const eb = this.#eb(EventKind.ContactList);
eb.content(JSON.stringify(relays));
const temp = new Set(follows.filter(a => a.length === 64).map(a => a.toLowerCase()));
temp.forEach(a => eb.tag(["p", a]));
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(["e", note.id, ""]);
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);
}
/**
* 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;
}
}