2023-01-15 19:40:47 +00:00
|
|
|
import { useSelector } from "react-redux";
|
2023-01-27 10:47:05 +00:00
|
|
|
|
2023-01-20 11:11:50 +00:00
|
|
|
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";
|
2023-01-27 21:10:14 +00:00
|
|
|
import { HexKey, RawEvent, u256, UserMetadata, Lists } from "Nostr";
|
2023-02-07 20:04:50 +00:00
|
|
|
import { bech32ToHex } from "Util";
|
2023-01-21 17:55:49 +00:00
|
|
|
import { DefaultRelays, HashtagRegex } from "Const";
|
2023-02-05 18:02:13 +00:00
|
|
|
import { RelaySettings } from "Nostr/Connection";
|
2023-01-15 19:40:47 +00:00
|
|
|
|
|
|
|
declare global {
|
2023-02-07 20:04:50 +00:00
|
|
|
interface Window {
|
|
|
|
nostr: {
|
|
|
|
getPublicKey: () => Promise<HexKey>;
|
|
|
|
signEvent: (event: RawEvent) => Promise<RawEvent>;
|
|
|
|
getRelays: () => Promise<
|
|
|
|
Record<string, { read: boolean; write: boolean }>
|
|
|
|
>;
|
|
|
|
nip04: {
|
|
|
|
encrypt: (pubkey: HexKey, content: string) => Promise<string>;
|
|
|
|
decrypt: (pubkey: HexKey, content: string) => Promise<string>;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
2023-01-15 19:40:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default function useEventPublisher() {
|
2023-02-07 20:04:50 +00:00
|
|
|
const pubKey = useSelector<RootState, HexKey | undefined>(
|
|
|
|
(s) => s.login.publicKey
|
|
|
|
);
|
|
|
|
const privKey = useSelector<RootState, HexKey | undefined>(
|
|
|
|
(s) => s.login.privateKey
|
|
|
|
);
|
|
|
|
const follows = useSelector<RootState, HexKey[]>((s) => s.login.follows);
|
|
|
|
const relays = useSelector((s: RootState) => s.login.relays);
|
|
|
|
const hasNip07 = "nostr" in window;
|
2023-01-15 19:40:47 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
async function signEvent(ev: NEvent): Promise<NEvent> {
|
|
|
|
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");
|
2023-01-15 19:40:47 +00:00
|
|
|
}
|
2023-02-07 20:04:50 +00:00
|
|
|
return ev;
|
|
|
|
}
|
2023-01-15 19:40:47 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
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 replaceNoteId = (match: string) => {
|
|
|
|
try {
|
|
|
|
const hex = bech32ToHex(match);
|
|
|
|
const idx = ev.Tags.length;
|
|
|
|
ev.Tags.push(new Tag(["e", hex, "", "mention"], 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 match;
|
|
|
|
};
|
|
|
|
const content = msg
|
|
|
|
.replace(/@npub[a-z0-9]+/g, replaceNpub)
|
|
|
|
.replace(/note[a-z0-9]+/g, replaceNoteId)
|
|
|
|
.replace(HashtagRegex, replaceHashtag);
|
|
|
|
ev.Content = content;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
nip42Auth: async (challenge: string, relay: string) => {
|
|
|
|
if (pubKey) {
|
|
|
|
const ev = NEvent.ForPubKey(pubKey);
|
|
|
|
ev.Kind = EventKind.Auth;
|
|
|
|
ev.Content = "";
|
|
|
|
ev.Tags.push(new Tag(["relay", relay], 0));
|
|
|
|
ev.Tags.push(new Tag(["challenge", challenge], 1));
|
|
|
|
return await signEvent(ev);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
broadcast: (ev: NEvent | undefined) => {
|
|
|
|
if (ev) {
|
|
|
|
console.debug("Sending event: ", ev);
|
|
|
|
System.BroadcastEvent(ev);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Write event to DefaultRelays, this is important for profiles / relay lists to prevent bugs
|
|
|
|
* If a user removes all the DefaultRelays from their relay list and saves that relay list,
|
|
|
|
* When they open the site again we wont see that updated relay list and so it will appear to reset back to the previous state
|
|
|
|
*/
|
|
|
|
broadcastForBootstrap: (ev: NEvent | undefined) => {
|
|
|
|
if (ev) {
|
|
|
|
for (let [k, _] of DefaultRelays) {
|
|
|
|
System.WriteOnceToRelay(k, ev);
|
2023-01-23 15:31:59 +00:00
|
|
|
}
|
2023-02-07 20:04:50 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
muted: async (keys: HexKey[], priv: HexKey[]) => {
|
|
|
|
if (pubKey) {
|
|
|
|
let ev = NEvent.ForPubKey(pubKey);
|
|
|
|
ev.Kind = EventKind.Lists;
|
|
|
|
ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length));
|
|
|
|
keys.forEach((p) => {
|
|
|
|
ev.Tags.push(new Tag(["p", p], ev.Tags.length));
|
|
|
|
});
|
|
|
|
let content = "";
|
|
|
|
if (priv.length > 0) {
|
|
|
|
const ps = priv.map((p) => ["p", p]);
|
|
|
|
const plaintext = JSON.stringify(ps);
|
|
|
|
if (hasNip07 && !privKey) {
|
|
|
|
content = await barierNip07(() =>
|
|
|
|
window.nostr.nip04.encrypt(pubKey, plaintext)
|
|
|
|
);
|
|
|
|
} else if (privKey) {
|
|
|
|
content = await ev.EncryptData(plaintext, pubKey, privKey);
|
|
|
|
}
|
2023-01-19 18:00:56 +00:00
|
|
|
}
|
2023-01-15 19:40:47 +00:00
|
|
|
ev.Content = content;
|
2023-02-07 20:04:50 +00:00
|
|
|
return await signEvent(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);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
zap: async (author: HexKey, note?: HexKey, msg?: string) => {
|
|
|
|
if (pubKey) {
|
|
|
|
let ev = NEvent.ForPubKey(pubKey);
|
|
|
|
ev.Kind = EventKind.ZapRequest;
|
|
|
|
if (note) {
|
|
|
|
// @ts-ignore
|
|
|
|
ev.Tags.push(new Tag(["e", note]));
|
|
|
|
}
|
|
|
|
// @ts-ignore
|
|
|
|
ev.Tags.push(new Tag(["p", author]));
|
|
|
|
// @ts-ignore
|
|
|
|
const relayTag = ["relays", ...Object.keys(relays).slice(0, 10)];
|
|
|
|
// @ts-ignore
|
|
|
|
ev.Tags.push(new Tag(relayTag));
|
|
|
|
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;
|
2023-01-15 19:40:47 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
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));
|
2023-01-15 19:40:47 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
// dont tag self in replies
|
|
|
|
if (replyTo.PubKey !== pubKey) {
|
|
|
|
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
|
|
|
|
}
|
2023-01-15 19:40:47 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
for (let pk of thread.PubKeys) {
|
|
|
|
if (pk === pubKey) {
|
|
|
|
continue; // dont tag self in replies
|
2023-01-15 19:40:47 +00:00
|
|
|
}
|
2023-02-07 20:04:50 +00:00
|
|
|
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));
|
|
|
|
}
|
2023-01-15 19:40:47 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
return await signEvent(ev);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
addFollow: async (
|
|
|
|
pkAdd: HexKey | HexKey[],
|
|
|
|
newRelays?: Record<string, RelaySettings>
|
|
|
|
) => {
|
|
|
|
if (pubKey) {
|
|
|
|
let ev = NEvent.ForPubKey(pubKey);
|
|
|
|
ev.Kind = EventKind.ContactList;
|
|
|
|
ev.Content = JSON.stringify(newRelays ?? 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) {
|
|
|
|
if (pk.length !== 64) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
|
|
|
}
|
2023-01-15 19:40:47 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
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 || pk.length !== 64) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
|
|
|
}
|
2023-01-15 19:40:47 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
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<string | undefined> => {
|
|
|
|
if (pubKey) {
|
|
|
|
if (
|
|
|
|
note.PubKey !== pubKey &&
|
|
|
|
!note.Tags.some((a) => a.PubKey === pubKey)
|
|
|
|
) {
|
|
|
|
return "<CANT DECRYPT>";
|
|
|
|
}
|
|
|
|
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 "<DECRYPTION FAILED>";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
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));
|
2023-01-15 19:40:47 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
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);
|
2023-01-15 19:40:47 +00:00
|
|
|
}
|
2023-02-07 20:04:50 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
2023-01-15 19:40:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let isNip07Busy = false;
|
|
|
|
|
|
|
|
const delay = (t: number) => {
|
2023-02-07 20:04:50 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
setTimeout(resolve, t);
|
|
|
|
});
|
|
|
|
};
|
2023-01-15 19:40:47 +00:00
|
|
|
|
2023-01-27 21:10:14 +00:00
|
|
|
export const barierNip07 = async (then: () => Promise<any>) => {
|
2023-02-07 20:04:50 +00:00
|
|
|
while (isNip07Busy) {
|
|
|
|
await delay(10);
|
|
|
|
}
|
|
|
|
isNip07Busy = true;
|
|
|
|
try {
|
|
|
|
return await then();
|
|
|
|
} finally {
|
|
|
|
isNip07Busy = false;
|
|
|
|
}
|
2023-01-27 17:35:55 +00:00
|
|
|
};
|