import { useSelector } from "react-redux"; import { System } from "Nostr/System"; import { default as NEvent } from "Nostr/Event"; import EventKind from "Nostr/EventKind"; import Tag from "Nostr/Tag"; import { RootState } from "State/Store"; import { HexKey, RawEvent, u256, UserMetadata } from "Nostr"; import { bech32ToHex } from "Util" import { HashtagRegex } from "Const"; declare global { interface Window { nostr: { getPublicKey: () => Promise, signEvent: (event: RawEvent) => Promise, getRelays: () => Promise<[[string, { read: boolean, write: boolean }]]>, nip04: { encrypt: (pubkey: HexKey, content: string) => Promise, decrypt: (pubkey: HexKey, content: string) => Promise } } } } export default function useEventPublisher() { const pubKey = useSelector(s => s.login.publicKey); const privKey = useSelector(s => s.login.privateKey); const follows = useSelector(s => s.login.follows); const relays = useSelector(s => s.login.relays); const hasNip07 = 'nostr' in window; async function signEvent(ev: NEvent): Promise { if (hasNip07 && !privKey) { ev.Id = await ev.CreateId(); let tmpEv = await barierNip07(() => window.nostr.signEvent(ev.ToObject())); return new NEvent(tmpEv); } else if (privKey) { await ev.Sign(privKey); } else { console.warn("Count not sign event, no private keys available"); } return ev; } function processContent(ev: NEvent, msg: string) { const replaceNpub = (match: string) => { const npub = match.slice(1); try { const hex = bech32ToHex(npub); const idx = ev.Tags.length; ev.Tags.push(new Tag(["p", hex], idx)); return `#[${idx}]` } catch (error) { return match } } const replaceHashtag = (match: string) => { const tag = match.slice(1); const idx = ev.Tags.length; ev.Tags.push(new Tag(["t", tag.toLowerCase()], idx)); return `#[${idx}]`; } let content = msg.replace(/@npub[a-z0-9]+/g, replaceNpub); content = content.replace(HashtagRegex, replaceHashtag); ev.Content = content; } return { broadcast: (ev: NEvent | undefined) => { if (ev) { console.debug("Sending event: ", ev); System.BroadcastEvent(ev); } }, metadata: async (obj: UserMetadata) => { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.SetMetadata; ev.Content = JSON.stringify(obj); return await signEvent(ev); } }, note: async (msg: string) => { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.TextNote; processContent(ev, msg); return await signEvent(ev); } }, /** * Reply to a note */ reply: async (replyTo: NEvent, msg: string) => { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.TextNote; let thread = replyTo.Thread; if (thread) { if (thread.Root || thread.ReplyTo) { ev.Tags.push(new Tag(["e", thread.Root?.Event ?? thread.ReplyTo?.Event!, "", "root"], ev.Tags.length)); } ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], ev.Tags.length)); // dont tag self in replies if (replyTo.PubKey !== pubKey) { ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length)); } for (let pk of thread.PubKeys) { if (pk === pubKey) { continue; // dont tag self in replies } ev.Tags.push(new Tag(["p", pk], ev.Tags.length)); } } else { ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], 0)); // dont tag self in replies if (replyTo.PubKey !== pubKey) { ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length)); } } processContent(ev, msg); return await signEvent(ev); } }, react: async (evRef: NEvent, content = "+") => { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.Reaction; ev.Content = content; ev.Tags.push(new Tag(["e", evRef.Id], 0)); ev.Tags.push(new Tag(["p", evRef.PubKey], 1)); return await signEvent(ev); } }, saveRelays: async () => { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.ContactList; ev.Content = JSON.stringify(relays); for (let pk of follows) { ev.Tags.push(new Tag(["p", pk], ev.Tags.length)); } return await signEvent(ev); } }, addFollow: async (pkAdd: HexKey | HexKey[]) => { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.ContactList; ev.Content = JSON.stringify(relays); let temp = new Set(follows); if (Array.isArray(pkAdd)) { pkAdd.forEach(a => temp.add(a)); } else { temp.add(pkAdd); } for (let pk of temp) { ev.Tags.push(new Tag(["p", pk], ev.Tags.length)); } return await signEvent(ev); } }, removeFollow: async (pkRemove: HexKey) => { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.ContactList; ev.Content = JSON.stringify(relays); for (let pk of follows) { if (pk === pkRemove) { continue; } ev.Tags.push(new Tag(["p", pk], ev.Tags.length)); } return await signEvent(ev); } }, /** * Delete an event (NIP-09) */ delete: async (id: u256) => { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.Deletion; ev.Content = ""; ev.Tags.push(new Tag(["e", id], 0)); return await signEvent(ev); } }, /** * Respot a note (NIP-18) */ repost: async (note: NEvent) => { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.Repost; ev.Content = JSON.stringify(note.Original); ev.Tags.push(new Tag(["e", note.Id], 0)); ev.Tags.push(new Tag(["p", note.PubKey], 1)); return await signEvent(ev); } }, decryptDm: async (note: NEvent): Promise => { if (pubKey) { if (note.PubKey !== pubKey && !note.Tags.some(a => a.PubKey === pubKey)) { return ""; } try { let otherPubKey = note.PubKey === pubKey ? note.Tags.filter(a => a.Key === "p")[0].PubKey! : note.PubKey; if (hasNip07 && !privKey) { return await barierNip07(() => window.nostr.nip04.decrypt(otherPubKey, note.Content)); } else if (privKey) { await note.DecryptDm(privKey, otherPubKey); return note.Content; } } catch (e) { console.error("Decyrption failed", e); return ""; } } }, sendDm: async (content: string, to: HexKey) => { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.DirectMessage; ev.Content = content; ev.Tags.push(new Tag(["p", to], 0)); try { if (hasNip07 && !privKey) { let cx: string = await barierNip07(() => window.nostr.nip04.encrypt(to, content)); ev.Content = cx; return await signEvent(ev); } else if (privKey) { await ev.EncryptDmForPubkey(to, privKey); return await signEvent(ev); } } catch (e) { console.error("Encryption failed", e); } } } } } let isNip07Busy = false; const delay = (t: number) => { return new Promise((resolve, reject) => { setTimeout(resolve, t); }); } const barierNip07 = async (then: () => Promise) => { while (isNip07Busy) { await delay(10); } isNip07Busy = true; try { return await then(); } finally { isNip07Busy = false; } };