Casual refactor of entire eventBuilder
This commit is contained in:
parent
914fa759a9
commit
36926d4346
@ -39,9 +39,9 @@ export const ProfileCacheExpire = 1_000 * 60 * 30;
|
||||
* Default bootstrap relays
|
||||
*/
|
||||
export const DefaultRelays = new Map<string, RelaySettings>([
|
||||
["wss://relay.snort.social", { read: true, write: true }],
|
||||
["wss://nostr.wine", { read: true, write: false }],
|
||||
["wss://nos.lol", { read: true, write: true }],
|
||||
["wss://relay.snort.social/", { read: true, write: true }],
|
||||
["wss://nostr.wine/", { read: true, write: false }],
|
||||
["wss://nos.lol/", { read: true, write: true }],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
@ -28,10 +28,12 @@ export default function DM(props: DMProps) {
|
||||
const otherPubkey = isMe ? pubKey : unwrap(props.data.tags.find(a => a[0] === "p")?.[1]);
|
||||
|
||||
async function decrypt() {
|
||||
const decrypted = await publisher.decryptDm(props.data);
|
||||
setContent(decrypted || "<ERROR>");
|
||||
if (!isMe) {
|
||||
setLastReadDm(props.data.pubkey);
|
||||
if (publisher) {
|
||||
const decrypted = await publisher.decryptDm(props.data);
|
||||
setContent(decrypted || "<ERROR>");
|
||||
if (!isMe) {
|
||||
setLastReadDm(props.data.pubkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,18 +14,26 @@ export interface FollowButtonProps {
|
||||
}
|
||||
export default function FollowButton(props: FollowButtonProps) {
|
||||
const pubkey = parseId(props.pubkey);
|
||||
const publiser = useEventPublisher();
|
||||
const isFollowing = useLogin().follows.item.includes(pubkey);
|
||||
const publisher = useEventPublisher();
|
||||
const { follows, relays } = useLogin();
|
||||
const isFollowing = follows.item.includes(pubkey);
|
||||
const baseClassname = `${props.className} follow-button`;
|
||||
|
||||
async function follow(pubkey: HexKey) {
|
||||
const ev = await publiser.addFollow(pubkey);
|
||||
publiser.broadcast(ev);
|
||||
if (publisher) {
|
||||
const ev = await publisher.contactList([pubkey, ...follows.item], relays.item);
|
||||
publisher.broadcast(ev);
|
||||
}
|
||||
}
|
||||
|
||||
async function unfollow(pubkey: HexKey) {
|
||||
const ev = await publiser.removeFollow(pubkey);
|
||||
publiser.broadcast(ev);
|
||||
if (publisher) {
|
||||
const ev = await publisher.contactList(
|
||||
follows.item.filter(a => a !== pubkey),
|
||||
relays.item
|
||||
);
|
||||
publisher.broadcast(ev);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -6,6 +6,7 @@ import { HexKey } from "@snort/nostr";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export interface FollowListBaseProps {
|
||||
pubkeys: HexKey[];
|
||||
@ -15,10 +16,13 @@ export interface FollowListBaseProps {
|
||||
}
|
||||
export default function FollowListBase({ pubkeys, title, showFollowAll, showAbout }: FollowListBaseProps) {
|
||||
const publisher = useEventPublisher();
|
||||
const { follows, relays } = useLogin();
|
||||
|
||||
async function followAll() {
|
||||
const ev = await publisher.addFollow(pubkeys);
|
||||
publisher.broadcast(ev);
|
||||
if (publisher) {
|
||||
const ev = await publisher.contactList([...pubkeys, ...follows.item], relays.item);
|
||||
publisher.broadcast(ev);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -189,7 +189,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
}
|
||||
|
||||
async function updateProfile(handle: string, domain: string) {
|
||||
if (user) {
|
||||
if (user && publisher) {
|
||||
const nip05 = `${handle}@${domain}`;
|
||||
const newProfile = {
|
||||
...user,
|
||||
|
@ -3,7 +3,7 @@ import React, { useMemo, useState, useLayoutEffect, ReactNode } from "react";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { TaggedRawEvent, HexKey, EventKind, NostrPrefix } from "@snort/nostr";
|
||||
import { TaggedRawEvent, HexKey, EventKind, NostrPrefix, Lists } from "@snort/nostr";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import Icon from "Icons/Icon";
|
||||
@ -132,27 +132,23 @@ export default function Note(props: NoteProps) {
|
||||
};
|
||||
|
||||
async function unpin(id: HexKey) {
|
||||
if (options.canUnpin) {
|
||||
if (options.canUnpin && publisher) {
|
||||
if (window.confirm(formatMessage(messages.ConfirmUnpin))) {
|
||||
const es = pinned.item.filter(e => e !== id);
|
||||
const ev = await publisher.pinned(es);
|
||||
if (ev) {
|
||||
publisher.broadcast(ev);
|
||||
setPinned(login, es, ev.created_at * 1000);
|
||||
}
|
||||
const ev = await publisher.noteList(es, Lists.Pinned);
|
||||
publisher.broadcast(ev);
|
||||
setPinned(login, es, ev.created_at * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function unbookmark(id: HexKey) {
|
||||
if (options.canUnbookmark) {
|
||||
if (options.canUnbookmark && publisher) {
|
||||
if (window.confirm(formatMessage(messages.ConfirmUnbookmark))) {
|
||||
const es = bookmarked.item.filter(e => e !== id);
|
||||
const ev = await publisher.bookmarked(es);
|
||||
if (ev) {
|
||||
publisher.broadcast(ev);
|
||||
setBookmarked(login, es, ev.created_at * 1000);
|
||||
}
|
||||
const ev = await publisher.noteList(es, Lists.Bookmarked);
|
||||
publisher.broadcast(ev);
|
||||
setBookmarked(login, es, ev.created_at * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import { LNURL } from "LNURL";
|
||||
import messages from "./messages";
|
||||
import { ClipboardEventHandler, useState } from "react";
|
||||
import Spinner from "Icons/Spinner";
|
||||
import { EventBuilder } from "System";
|
||||
|
||||
interface NotePreviewProps {
|
||||
note: TaggedRawEvent;
|
||||
@ -64,7 +65,7 @@ export function NoteCreator() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
async function sendNote() {
|
||||
if (note) {
|
||||
if (note && publisher) {
|
||||
let extraTags: Array<Array<string>> | undefined;
|
||||
if (zapForward) {
|
||||
try {
|
||||
@ -91,9 +92,12 @@ export function NoteCreator() {
|
||||
extraTags ??= [];
|
||||
extraTags.push(...pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
|
||||
}
|
||||
const ev = replyTo
|
||||
? await publisher.reply(replyTo, note, extraTags, kind)
|
||||
: await publisher.note(note, extraTags, kind);
|
||||
const hk = (eb: EventBuilder) => {
|
||||
extraTags?.forEach(t => eb.tag(t));
|
||||
eb.kind(kind);
|
||||
return eb;
|
||||
};
|
||||
const ev = replyTo ? await publisher.reply(replyTo, note, hk) : await publisher.note(note, hk);
|
||||
publisher.broadcast(ev);
|
||||
dispatch(reset());
|
||||
}
|
||||
@ -154,7 +158,7 @@ export function NoteCreator() {
|
||||
async function loadPreview() {
|
||||
if (preview) {
|
||||
dispatch(setPreview(undefined));
|
||||
} else {
|
||||
} else if (publisher) {
|
||||
const tmpNote = await publisher.note(note);
|
||||
if (tmpNote) {
|
||||
dispatch(setPreview(tmpNote));
|
||||
|
@ -3,7 +3,7 @@ import { useSelector, useDispatch } from "react-redux";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
import { useLongPress } from "use-long-press";
|
||||
import { TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix } from "@snort/nostr";
|
||||
import { TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists } from "@snort/nostr";
|
||||
|
||||
import Icon from "Icons/Icon";
|
||||
import Spinner from "Icons/Spinner";
|
||||
@ -96,7 +96,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { formatMessage } = useIntl();
|
||||
const login = useLogin();
|
||||
const { pinned, bookmarked, publicKey, preferences: prefs } = login;
|
||||
const { pinned, bookmarked, publicKey, preferences: prefs, relays } = login;
|
||||
const { mute, block } = useModeration();
|
||||
const author = useUserProfile(ev.pubkey);
|
||||
const publisher = useEventPublisher();
|
||||
@ -134,21 +134,21 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}
|
||||
|
||||
async function react(content: string) {
|
||||
if (!hasReacted(content)) {
|
||||
if (!hasReacted(content) && publisher) {
|
||||
const evLike = await publisher.react(ev, content);
|
||||
publisher.broadcast(evLike);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEvent() {
|
||||
if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.id.substring(0, 8) }))) {
|
||||
if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.id.substring(0, 8) })) && publisher) {
|
||||
const evDelete = await publisher.delete(ev.id);
|
||||
publisher.broadcast(evDelete);
|
||||
}
|
||||
}
|
||||
|
||||
async function repost() {
|
||||
if (!hasReposted()) {
|
||||
if (!hasReposted() && publisher) {
|
||||
if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) {
|
||||
const evRepost = await publisher.repost(ev);
|
||||
publisher.broadcast(evRepost);
|
||||
@ -196,7 +196,9 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
await barrierZapper(async () => {
|
||||
const handler = new LNURL(lnurl);
|
||||
await handler.load();
|
||||
const zap = handler.canZap ? await publisher.zap(amount * 1000, key, id) : undefined;
|
||||
|
||||
const zr = Object.keys(relays.item);
|
||||
const zap = handler.canZap && publisher ? await publisher.zap(amount * 1000, key, zr, id) : undefined;
|
||||
const invoice = await handler.getInvoice(amount, undefined, zap);
|
||||
await wallet?.payInvoice(unwrap(invoice.pr));
|
||||
});
|
||||
@ -320,18 +322,18 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}
|
||||
|
||||
async function pin(id: HexKey) {
|
||||
const es = [...pinned.item, id];
|
||||
const ev = await publisher.pinned(es);
|
||||
if (ev) {
|
||||
if (publisher) {
|
||||
const es = [...pinned.item, id];
|
||||
const ev = await publisher.noteList(es, Lists.Pinned);
|
||||
publisher.broadcast(ev);
|
||||
setPinned(login, es, ev.created_at * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function bookmark(id: HexKey) {
|
||||
const es = [...bookmarked.item, id];
|
||||
const ev = await publisher.bookmarked(es);
|
||||
if (ev) {
|
||||
if (publisher) {
|
||||
const es = [...bookmarked.item, id];
|
||||
const ev = await publisher.noteList(es, Lists.Bookmarked);
|
||||
publisher.broadcast(ev);
|
||||
setBookmarked(login, es, ev.created_at * 1000);
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ export default function Poll(props: PollProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const { wallet } = useWallet();
|
||||
const { preferences: prefs, publicKey: myPubKey } = useLogin();
|
||||
const { preferences: prefs, publicKey: myPubKey, relays } = useLogin();
|
||||
const pollerProfile = useUserProfile(props.ev.pubkey);
|
||||
const [error, setError] = useState("");
|
||||
const [invoice, setInvoice] = useState("");
|
||||
@ -35,7 +35,7 @@ export default function Poll(props: PollProps) {
|
||||
const options = props.ev.tags.filter(a => a[0] === "poll_option").sort((a, b) => Number(a[1]) - Number(b[1]));
|
||||
async function zapVote(ev: React.MouseEvent, opt: number) {
|
||||
ev.stopPropagation();
|
||||
if (voting) return;
|
||||
if (voting || !publisher) return;
|
||||
|
||||
const amount = prefs.defaultZapAmount;
|
||||
try {
|
||||
@ -53,17 +53,10 @@ export default function Poll(props: PollProps) {
|
||||
}
|
||||
|
||||
setVoting(opt);
|
||||
const zap = await publisher.zap(amount * 1000, props.ev.pubkey, props.ev.id, undefined, [
|
||||
["poll_option", opt.toString()],
|
||||
]);
|
||||
|
||||
if (!zap) {
|
||||
throw new Error(
|
||||
formatMessage({
|
||||
defaultMessage: "Can't create vote, maybe you're not logged in?",
|
||||
})
|
||||
);
|
||||
}
|
||||
const r = Object.keys(relays.item);
|
||||
const zap = await publisher.zap(amount * 1000, props.ev.pubkey, r, props.ev.id, undefined, eb =>
|
||||
eb.tag(["poll_option", opt.toString()])
|
||||
);
|
||||
|
||||
const lnurl = props.ev.tags.find(a => a[0] === "zap")?.[1] || pollerProfile?.lud16 || pollerProfile?.lud06;
|
||||
if (!lnurl) return;
|
||||
|
@ -13,10 +13,11 @@ import Copy from "Element/Copy";
|
||||
import { LNURL, LNURLError, LNURLErrorCode, LNURLInvoice, LNURLSuccessAction } from "LNURL";
|
||||
import { chunks, debounce } from "Util";
|
||||
import { useWallet } from "Wallet";
|
||||
import { EventExt } from "System/EventExt";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { generateRandomKey } from "Login";
|
||||
import { EventPublisher } from "System/EventPublisher";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
enum ZapType {
|
||||
PublicZap = 1,
|
||||
@ -40,7 +41,8 @@ export interface SendSatsProps {
|
||||
export default function SendSats(props: SendSatsProps) {
|
||||
const onClose = props.onClose || (() => undefined);
|
||||
const { note, author, target } = props;
|
||||
const defaultZapAmount = useLogin().preferences.defaultZapAmount;
|
||||
const login = useLogin();
|
||||
const defaultZapAmount = login.preferences.defaultZapAmount;
|
||||
const amounts = [defaultZapAmount, 1_000, 5_000, 10_000, 20_000, 50_000, 100_000, 1_000_000];
|
||||
const emojis: Record<number, string> = {
|
||||
1_000: "👍",
|
||||
@ -118,22 +120,21 @@ export default function SendSats(props: SendSatsProps) {
|
||||
};
|
||||
|
||||
async function loadInvoice() {
|
||||
if (!amount || !handler) return null;
|
||||
if (!amount || !handler || !publisher) return null;
|
||||
|
||||
let zap: RawEvent | undefined;
|
||||
if (author && zapType !== ZapType.NonZap) {
|
||||
const ev = await publisher.zap(amount * 1000, author, note, comment);
|
||||
if (ev) {
|
||||
// replace sig for anon-zap
|
||||
if (zapType === ZapType.AnonZap) {
|
||||
const randomKey = publisher.newKey();
|
||||
console.debug("Generated new key for zap: ", randomKey);
|
||||
ev.pubkey = randomKey.publicKey;
|
||||
ev.id = "";
|
||||
ev.tags.push(["anon", ""]);
|
||||
await EventExt.sign(ev, randomKey.privateKey);
|
||||
}
|
||||
zap = ev;
|
||||
const relays = Object.keys(login.relays.item);
|
||||
|
||||
// use random key for anon zaps
|
||||
if (zapType === ZapType.AnonZap) {
|
||||
const randomKey = generateRandomKey();
|
||||
console.debug("Generated new key for zap: ", randomKey);
|
||||
|
||||
const publisher = new EventPublisher(randomKey.publicKey, randomKey.privateKey);
|
||||
zap = await publisher.zap(amount * 1000, author, relays, note, comment);
|
||||
} else {
|
||||
zap = await publisher.zap(amount * 1000, author, relays, note, comment);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,418 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import { EventKind, RelaySettings, TaggedRawEvent, HexKey, RawEvent, u256, UserMetadata, Lists } from "@snort/nostr";
|
||||
|
||||
import { bech32ToHex, delay, unwrap } from "Util";
|
||||
import { DefaultRelays, HashtagRegex } from "Const";
|
||||
import { System } from "System";
|
||||
import { EventExt } from "System/EventExt";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
declare global {
|
||||
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>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type EventPublisher = ReturnType<typeof useEventPublisher>;
|
||||
import { EventPublisher } from "System/EventPublisher";
|
||||
|
||||
export default function useEventPublisher() {
|
||||
const { publicKey: pubKey, privateKey: privKey, follows, relays } = useLogin();
|
||||
const hasNip07 = "nostr" in window;
|
||||
|
||||
async function signEvent(ev: RawEvent): Promise<RawEvent> {
|
||||
if (!pubKey) {
|
||||
throw new Error("Cant sign events when logged out");
|
||||
const { publicKey, privateKey } = useLogin();
|
||||
return useMemo(() => {
|
||||
if (publicKey) {
|
||||
return new EventPublisher(publicKey, privateKey);
|
||||
}
|
||||
|
||||
if (hasNip07 && !privKey) {
|
||||
ev.id = await EventExt.createId(ev);
|
||||
const tmpEv = (await barrierNip07(() => window.nostr.signEvent(ev))) as RawEvent;
|
||||
ev.sig = tmpEv.sig;
|
||||
return ev;
|
||||
} else if (privKey) {
|
||||
await EventExt.sign(ev, privKey);
|
||||
} else {
|
||||
console.warn("Count not sign event, no private keys available");
|
||||
}
|
||||
return ev;
|
||||
}
|
||||
|
||||
function processContent(ev: RawEvent, msg: string) {
|
||||
const replaceNpub = (match: string) => {
|
||||
const npub = match.slice(1);
|
||||
try {
|
||||
const hex = bech32ToHex(npub);
|
||||
const idx = ev.tags.length;
|
||||
ev.tags.push(["p", hex]);
|
||||
return `#[${idx}]`;
|
||||
} catch (error) {
|
||||
return match;
|
||||
}
|
||||
};
|
||||
const replaceNoteId = (match: string) => {
|
||||
const noteId = match.slice(1);
|
||||
try {
|
||||
const hex = bech32ToHex(noteId);
|
||||
const idx = ev.tags.length;
|
||||
ev.tags.push(["e", hex, "", "mention"]);
|
||||
return `#[${idx}]`;
|
||||
} catch (error) {
|
||||
return match;
|
||||
}
|
||||
};
|
||||
const replaceHashtag = (match: string) => {
|
||||
const tag = match.slice(1);
|
||||
ev.tags.push(["t", tag.toLowerCase()]);
|
||||
return match;
|
||||
};
|
||||
const content = msg
|
||||
.replace(/@npub[a-z0-9]+/g, replaceNpub)
|
||||
.replace(/@note1[acdefghjklmnpqrstuvwxyz023456789]{58}/g, replaceNoteId)
|
||||
.replace(HashtagRegex, replaceHashtag);
|
||||
ev.content = content;
|
||||
}
|
||||
|
||||
const ret = {
|
||||
nip42Auth: async (challenge: string, relay: string) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.Auth);
|
||||
ev.tags.push(["relay", relay]);
|
||||
ev.tags.push(["challenge", challenge]);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
broadcast: (ev: RawEvent | undefined) => {
|
||||
if (ev) {
|
||||
console.debug(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: RawEvent | undefined) => {
|
||||
if (ev) {
|
||||
for (const [k] of DefaultRelays) {
|
||||
System.WriteOnceToRelay(k, ev);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Write event to all given relays.
|
||||
*/
|
||||
broadcastAll: (ev: RawEvent | undefined, relays: string[]) => {
|
||||
if (ev) {
|
||||
for (const k of relays) {
|
||||
System.WriteOnceToRelay(k, ev);
|
||||
}
|
||||
}
|
||||
},
|
||||
muted: async (keys: HexKey[], priv: HexKey[]) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.PubkeyLists);
|
||||
ev.tags.push(["d", Lists.Muted]);
|
||||
keys.forEach(p => {
|
||||
ev.tags.push(["p", p]);
|
||||
});
|
||||
let content = "";
|
||||
if (priv.length > 0) {
|
||||
const ps = priv.map(p => ["p", p]);
|
||||
const plaintext = JSON.stringify(ps);
|
||||
if (hasNip07 && !privKey) {
|
||||
content = await barrierNip07(() => window.nostr.nip04.encrypt(pubKey, plaintext));
|
||||
} else if (privKey) {
|
||||
content = await EventExt.encryptData(plaintext, pubKey, privKey);
|
||||
}
|
||||
}
|
||||
ev.content = content;
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
pinned: async (notes: HexKey[]) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.NoteLists);
|
||||
ev.tags.push(["d", Lists.Pinned]);
|
||||
notes.forEach(n => {
|
||||
ev.tags.push(["e", n]);
|
||||
});
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
bookmarked: async (notes: HexKey[]) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.NoteLists);
|
||||
ev.tags.push(["d", Lists.Bookmarked]);
|
||||
notes.forEach(n => {
|
||||
ev.tags.push(["e", n]);
|
||||
});
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
tags: async (tags: string[]) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.TagLists);
|
||||
ev.tags.push(["d", Lists.Followed]);
|
||||
tags.forEach(t => {
|
||||
ev.tags.push(["t", t]);
|
||||
});
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
metadata: async (obj: UserMetadata) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.SetMetadata);
|
||||
ev.content = JSON.stringify(obj);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
note: async (msg: string, extraTags?: Array<Array<string>>, kind?: EventKind) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, kind ?? EventKind.TextNote);
|
||||
processContent(ev, msg);
|
||||
if (extraTags) {
|
||||
for (const et of extraTags) {
|
||||
ev.tags.push(et);
|
||||
}
|
||||
}
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 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
|
||||
* @param extraTags Any extra tags to include on the zap request event
|
||||
* @returns
|
||||
*/
|
||||
zap: async (amount: number, author: HexKey, note?: HexKey, msg?: string, extraTags?: Array<Array<string>>) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.ZapRequest);
|
||||
if (note) {
|
||||
ev.tags.push(["e", note]);
|
||||
}
|
||||
ev.tags.push(["p", author]);
|
||||
const relayTag = ["relays", ...Object.keys(relays).map(a => a.trim())];
|
||||
ev.tags.push(relayTag);
|
||||
ev.tags.push(["amount", amount.toString()]);
|
||||
ev.tags.push(...(extraTags ?? []));
|
||||
processContent(ev, msg || "");
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Reply to a note
|
||||
*/
|
||||
reply: async (replyTo: TaggedRawEvent, msg: string, extraTags?: Array<Array<string>>, kind?: EventKind) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, kind ?? EventKind.TextNote);
|
||||
|
||||
const thread = EventExt.extractThread(ev);
|
||||
if (thread) {
|
||||
if (thread.root || thread.replyTo) {
|
||||
ev.tags.push(["e", thread.root?.Event ?? thread.replyTo?.Event ?? "", "", "root"]);
|
||||
}
|
||||
ev.tags.push(["e", replyTo.id, replyTo.relays[0] ?? "", "reply"]);
|
||||
|
||||
// dont tag self in replies
|
||||
if (replyTo.pubkey !== pubKey) {
|
||||
ev.tags.push(["p", replyTo.pubkey]);
|
||||
}
|
||||
|
||||
for (const pk of thread.pubKeys) {
|
||||
if (pk === pubKey) {
|
||||
continue; // dont tag self in replies
|
||||
}
|
||||
ev.tags.push(["p", pk]);
|
||||
}
|
||||
} else {
|
||||
ev.tags.push(["e", replyTo.id, "", "reply"]);
|
||||
// dont tag self in replies
|
||||
if (replyTo.pubkey !== pubKey) {
|
||||
ev.tags.push(["p", replyTo.pubkey]);
|
||||
}
|
||||
}
|
||||
processContent(ev, msg);
|
||||
if (extraTags) {
|
||||
for (const et of extraTags) {
|
||||
ev.tags.push(et);
|
||||
}
|
||||
}
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
react: async (evRef: RawEvent, content = "+") => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.Reaction);
|
||||
ev.content = content;
|
||||
ev.tags.push(["e", evRef.id]);
|
||||
ev.tags.push(["p", evRef.pubkey]);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
saveRelays: async () => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.ContactList);
|
||||
ev.content = JSON.stringify(relays);
|
||||
for (const pk of follows.item) {
|
||||
ev.tags.push(["p", pk]);
|
||||
}
|
||||
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
saveRelaysSettings: async () => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.Relays);
|
||||
for (const [url, settings] of Object.entries(relays)) {
|
||||
const rTag = ["r", url];
|
||||
if (settings.read && !settings.write) {
|
||||
rTag.push("read");
|
||||
}
|
||||
if (settings.write && !settings.read) {
|
||||
rTag.push("write");
|
||||
}
|
||||
ev.tags.push(rTag);
|
||||
}
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
addFollow: async (pkAdd: HexKey | HexKey[], newRelays?: Record<string, RelaySettings>) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.ContactList);
|
||||
ev.content = JSON.stringify(newRelays ?? relays);
|
||||
const temp = new Set(follows.item);
|
||||
if (Array.isArray(pkAdd)) {
|
||||
pkAdd.forEach(a => temp.add(a));
|
||||
} else {
|
||||
temp.add(pkAdd);
|
||||
}
|
||||
for (const pk of temp) {
|
||||
if (pk.length !== 64) {
|
||||
continue;
|
||||
}
|
||||
ev.tags.push(["p", pk.toLowerCase()]);
|
||||
}
|
||||
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
removeFollow: async (pkRemove: HexKey) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.ContactList);
|
||||
ev.content = JSON.stringify(relays);
|
||||
for (const pk of follows.item) {
|
||||
if (pk === pkRemove || pk.length !== 64) {
|
||||
continue;
|
||||
}
|
||||
ev.tags.push(["p", pk]);
|
||||
}
|
||||
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Delete an event (NIP-09)
|
||||
*/
|
||||
delete: async (id: u256) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.Deletion);
|
||||
ev.tags.push(["e", id]);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Repost a note (NIP-18)
|
||||
*/
|
||||
repost: async (note: TaggedRawEvent) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.Repost);
|
||||
ev.tags.push(["e", note.id, ""]);
|
||||
ev.tags.push(["p", note.pubkey]);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
decryptDm: async (note: RawEvent): Promise<string | undefined> => {
|
||||
if (pubKey) {
|
||||
if (note.pubkey !== pubKey && !note.tags.some(a => a[1] === pubKey)) {
|
||||
return "<CANT DECRYPT>";
|
||||
}
|
||||
try {
|
||||
const otherPubKey = note.pubkey === pubKey ? unwrap(note.tags.find(a => a[0] === "p")?.[1]) : note.pubkey;
|
||||
if (hasNip07 && !privKey) {
|
||||
return await barrierNip07(() => window.nostr.nip04.decrypt(otherPubKey, note.content));
|
||||
} else if (privKey) {
|
||||
return await EventExt.decryptDm(note.content, privKey, otherPubKey);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Decryption failed", e);
|
||||
return "<DECRYPTION FAILED>";
|
||||
}
|
||||
}
|
||||
},
|
||||
sendDm: async (content: string, to: HexKey) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.DirectMessage);
|
||||
ev.content = content;
|
||||
ev.tags.push(["p", to]);
|
||||
|
||||
try {
|
||||
if (hasNip07 && !privKey) {
|
||||
const cx: string = await barrierNip07(() => window.nostr.nip04.encrypt(to, content));
|
||||
ev.content = cx;
|
||||
return await signEvent(ev);
|
||||
} else if (privKey) {
|
||||
ev.content = await EventExt.encryptData(content, to, privKey);
|
||||
return await signEvent(ev);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Encryption failed", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
newKey: () => {
|
||||
const privKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey());
|
||||
const pubKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(privKey));
|
||||
return {
|
||||
privateKey: privKey,
|
||||
publicKey: pubKey,
|
||||
};
|
||||
},
|
||||
generic: async (content: string, kind: EventKind, tags?: Array<Array<string>>) => {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, kind);
|
||||
ev.content = content;
|
||||
ev.tags = tags ?? [];
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return useMemo(() => ret, [pubKey, relays, follows]);
|
||||
}, [publicKey, privateKey]);
|
||||
}
|
||||
|
||||
let isNip07Busy = false;
|
||||
|
||||
export const barrierNip07 = async <T>(then: () => Promise<T>): Promise<T> => {
|
||||
while (isNip07Busy) {
|
||||
await delay(10);
|
||||
}
|
||||
isNip07Busy = true;
|
||||
try {
|
||||
return await then();
|
||||
} finally {
|
||||
isNip07Busy = false;
|
||||
}
|
||||
};
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { TaggedRawEvent, HexKey, Lists, EventKind } from "@snort/nostr";
|
||||
import { TaggedRawEvent, Lists, EventKind } from "@snort/nostr";
|
||||
|
||||
import { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "Util";
|
||||
import { makeNotification, sendNotification } from "Notifications";
|
||||
import useEventPublisher, { barrierNip07 } from "Feed/EventPublisher";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { getMutedKeys } from "Feed/MuteList";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { FlatNoteStore, RequestBuilder } from "System";
|
||||
import useRequestBuilder from "Hooks/useRequestBuilder";
|
||||
import { EventExt } from "System/EventExt";
|
||||
import { DmCache } from "Cache";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { addSubscription, setBlocked, setBookmarked, setFollows, setMuted, setPinned, setRelays, setTags } from "Login";
|
||||
@ -20,7 +19,7 @@ import { SubscriptionEvent } from "Subscription";
|
||||
*/
|
||||
export default function useLoginFeed() {
|
||||
const login = useLogin();
|
||||
const { publicKey: pubKey, privateKey: privKey, readNotifications } = login;
|
||||
const { publicKey: pubKey, readNotifications } = login;
|
||||
const { isMuted } = useModeration();
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
@ -63,7 +62,7 @@ export default function useLoginFeed() {
|
||||
|
||||
// update relays and follow lists
|
||||
useEffect(() => {
|
||||
if (loginFeed.data) {
|
||||
if (loginFeed.data && publisher) {
|
||||
const contactList = getNewest(loginFeed.data.filter(a => a.kind === EventKind.ContactList));
|
||||
if (contactList) {
|
||||
if (contactList.content !== "" && contactList.content !== "{}") {
|
||||
@ -93,7 +92,7 @@ export default function useLoginFeed() {
|
||||
})
|
||||
).then(a => addSubscription(login, ...a.filter(a => a !== undefined).map(unwrap)));
|
||||
}
|
||||
}, [loginFeed]);
|
||||
}, [loginFeed, publisher]);
|
||||
|
||||
// send out notifications
|
||||
useEffect(() => {
|
||||
@ -115,7 +114,8 @@ export default function useLoginFeed() {
|
||||
setMuted(login, muted.keys, muted.createdAt * 1000);
|
||||
|
||||
if (muted.raw && (muted.raw?.content?.length ?? 0) > 0 && pubKey) {
|
||||
decryptBlocked(muted.raw, pubKey, privKey)
|
||||
publisher
|
||||
?.nip4Decrypt(muted.raw.content, pubKey)
|
||||
.then(plaintext => {
|
||||
try {
|
||||
const blocked = JSON.parse(plaintext);
|
||||
@ -176,11 +176,3 @@ export default function useLoginFeed() {
|
||||
FollowsRelays.bulkSet(fRelays).catch(console.error);
|
||||
}, [dispatch, fRelays]);*/
|
||||
}
|
||||
|
||||
async function decryptBlocked(raw: TaggedRawEvent, pubKey: HexKey, privKey?: HexKey) {
|
||||
if (pubKey && privKey) {
|
||||
return await EventExt.decryptData(raw.content, privKey, pubKey);
|
||||
} else {
|
||||
return await barrierNip07(() => window.nostr.nip04.decrypt(pubKey, raw.content));
|
||||
}
|
||||
}
|
||||
|
@ -10,14 +10,10 @@ export default function useModeration() {
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
async function setMutedList(pub: HexKey[], priv: HexKey[]) {
|
||||
try {
|
||||
if (publisher) {
|
||||
const ev = await publisher.muted(pub, priv);
|
||||
if (ev) {
|
||||
publisher.broadcast(ev);
|
||||
return ev.created_at * 1000;
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug("Couldn't change mute list");
|
||||
publisher.broadcast(ev);
|
||||
return ev.created_at * 1000;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ import { HexKey, RelaySettings } from "@snort/nostr";
|
||||
import * as secp from "@noble/secp256k1";
|
||||
|
||||
import { DefaultRelays, SnortPubKey } from "Const";
|
||||
import { EventPublisher } from "Feed/EventPublisher";
|
||||
import { LoginStore, UserPreferences, LoginSession } from "Login";
|
||||
import { generateBip39Entropy, entropyToDerivedKey } from "nip6";
|
||||
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unixNowMs } from "Util";
|
||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unixNowMs, unwrap } from "Util";
|
||||
import { getCurrentSubscription, SubscriptionEvent } from "Subscription";
|
||||
import { EventPublisher } from "System/EventPublisher";
|
||||
|
||||
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {
|
||||
if (state.relays.timestamp > createdAt) {
|
||||
@ -55,10 +55,10 @@ export function clearEntropy(state: LoginSession) {
|
||||
/**
|
||||
* Generate a new key and login with this generated key
|
||||
*/
|
||||
export async function generateNewLogin(publisher: EventPublisher) {
|
||||
export async function generateNewLogin() {
|
||||
const ent = generateBip39Entropy();
|
||||
const entHex = secp.utils.bytesToHex(ent);
|
||||
const newKeyHex = entropyToDerivedKey(ent);
|
||||
const entropy = secp.utils.bytesToHex(ent);
|
||||
const privateKey = entropyToPrivateKey(ent);
|
||||
let newRelays: Record<string, RelaySettings> = {};
|
||||
|
||||
try {
|
||||
@ -66,7 +66,7 @@ export async function generateNewLogin(publisher: EventPublisher) {
|
||||
if (rsp.ok) {
|
||||
const online: string[] = await rsp.json();
|
||||
const pickRandom = randomSample(online, 4);
|
||||
const relayObjects = pickRandom.map(a => [a, { read: true, write: true }]);
|
||||
const relayObjects = pickRandom.map(a => [unwrap(sanitizeRelayUrl(a)), { read: true, write: true }]);
|
||||
newRelays = {
|
||||
...Object.fromEntries(relayObjects),
|
||||
...Object.fromEntries(DefaultRelays.entries()),
|
||||
@ -76,10 +76,21 @@ export async function generateNewLogin(publisher: EventPublisher) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
const ev = await publisher.addFollow([bech32ToHex(SnortPubKey), newKeyHex], newRelays);
|
||||
const publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(privateKey));
|
||||
const publisher = new EventPublisher(publicKey, privateKey);
|
||||
const ev = await publisher.contactList([bech32ToHex(SnortPubKey), publicKey], newRelays);
|
||||
publisher.broadcast(ev);
|
||||
|
||||
LoginStore.loginWithPrivateKey(newKeyHex, entHex);
|
||||
LoginStore.loginWithPrivateKey(privateKey, entropy, newRelays);
|
||||
}
|
||||
|
||||
export function generateRandomKey() {
|
||||
const privateKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey());
|
||||
const publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(privateKey));
|
||||
return {
|
||||
privateKey,
|
||||
publicKey,
|
||||
};
|
||||
}
|
||||
|
||||
export function setTags(state: LoginSession, tags: Array<string>, ts: number) {
|
||||
|
@ -40,7 +40,7 @@ const LoggedOut = {
|
||||
},
|
||||
latestNotification: 0,
|
||||
readNotifications: 0,
|
||||
subscriptions: []
|
||||
subscriptions: [],
|
||||
} as LoginSession;
|
||||
const LegacyKeys = {
|
||||
PrivateKeyItem: "secret",
|
||||
@ -94,20 +94,27 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
return newSession;
|
||||
}
|
||||
|
||||
loginWithPrivateKey(key: HexKey, entropy?: string) {
|
||||
loginWithPrivateKey(key: HexKey, entropy?: string, relays?: Record<string, RelaySettings>) {
|
||||
const pubKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(key));
|
||||
if (this.#accounts.has(pubKey)) {
|
||||
throw new Error("Already logged in with this pubkey");
|
||||
}
|
||||
this.#accounts.set(pubKey, {
|
||||
const initRelays = relays ?? Object.fromEntries(DefaultRelays.entries());
|
||||
const newSession = {
|
||||
...LoggedOut,
|
||||
privateKey: key,
|
||||
publicKey: pubKey,
|
||||
generatedEntropy: entropy,
|
||||
relays: {
|
||||
item: initRelays,
|
||||
timestamp: 1,
|
||||
},
|
||||
preferences: deepClone(DefaultPreferences),
|
||||
} as LoginSession);
|
||||
} as LoginSession;
|
||||
this.#accounts.set(pubKey, newSession);
|
||||
this.#activeAccount = pubKey;
|
||||
this.#save();
|
||||
return newSession;
|
||||
}
|
||||
|
||||
updateSession(s: LoginSession) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { EventKind } from "@snort/nostr";
|
||||
import { EventPublisher } from "Feed/EventPublisher";
|
||||
import { EventPublisher } from "System/EventPublisher";
|
||||
import { ServiceError, ServiceProvider } from "./ServiceProvider";
|
||||
|
||||
export interface ManageHandle {
|
||||
@ -48,10 +48,12 @@ export default class SnortServiceProvider extends ServiceProvider {
|
||||
body?: unknown,
|
||||
headers?: { [key: string]: string }
|
||||
): Promise<T | ServiceError> {
|
||||
const auth = await this.#publisher.generic("", EventKind.HttpAuthentication, [
|
||||
["url", `${this.url}${path}`],
|
||||
["method", method ?? "GET"],
|
||||
]);
|
||||
const auth = await this.#publisher.generic(eb => {
|
||||
eb.kind(EventKind.HttpAuthentication);
|
||||
eb.tag(["url", `${this.url}${path}`]);
|
||||
eb.tag(["method", method ?? "GET"]);
|
||||
return eb;
|
||||
});
|
||||
if (!auth) {
|
||||
return {
|
||||
error: "INVALID_TOKEN",
|
||||
|
@ -41,9 +41,8 @@ export default function ChatPage() {
|
||||
}, [dmListRef.current?.scrollHeight]);
|
||||
|
||||
async function sendDm() {
|
||||
if (content) {
|
||||
if (content && publisher) {
|
||||
const ev = await publisher.sendDm(content, id);
|
||||
console.debug(ev);
|
||||
publisher.broadcast(ev);
|
||||
setContent("");
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ const HashTagsPage = () => {
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
async function followTags(ts: string[]) {
|
||||
const ev = await publisher.tags(ts);
|
||||
if (ev) {
|
||||
if (publisher) {
|
||||
const ev = await publisher.tags(ts);
|
||||
publisher.broadcast(ev);
|
||||
setTags(login, ts, ev.created_at * 1000);
|
||||
}
|
||||
|
@ -63,7 +63,9 @@ export default function Layout() {
|
||||
}, [location]);
|
||||
|
||||
useEffect(() => {
|
||||
System.HandleAuth = pub.nip42Auth;
|
||||
if (pub) {
|
||||
System.HandleAuth = pub.nip42Auth;
|
||||
}
|
||||
}, [pub]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -224,7 +226,14 @@ const AccountHeader = () => {
|
||||
<Icon name="bell" />
|
||||
{hasNotifications && <span className="has-unread"></span>}
|
||||
</div>
|
||||
{profile && <Avatar user={profile} onClick={() => navigate(profileLink(profile.pubkey))} />}
|
||||
<Avatar
|
||||
user={profile}
|
||||
onClick={() => {
|
||||
if (profile) {
|
||||
navigate(profileLink(profile.pubkey));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -8,13 +8,12 @@ import { HexKey } from "@snort/nostr";
|
||||
|
||||
import { EmailRegex, MnemonicRegex } from "Const";
|
||||
import { bech32ToHex, unwrap } from "Util";
|
||||
import { generateBip39Entropy, entropyToDerivedKey } from "nip6";
|
||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||
import ZapButton from "Element/ZapButton";
|
||||
import useImgProxy from "Hooks/useImgProxy";
|
||||
import Icon from "Icons/Icon";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { generateNewLogin, LoginStore } from "Login";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
|
||||
import messages from "./messages";
|
||||
@ -68,7 +67,6 @@ export async function getNip05PubKey(addr: string): Promise<string> {
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const publisher = useEventPublisher();
|
||||
const login = useLogin();
|
||||
const [key, setKey] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
@ -117,7 +115,7 @@ export default function LoginPage() {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
const ent = generateBip39Entropy(key);
|
||||
const keyHex = entropyToDerivedKey(ent);
|
||||
const keyHex = entropyToPrivateKey(ent);
|
||||
LoginStore.loginWithPrivateKey(keyHex);
|
||||
} else if (secp.utils.isValidPrivateKey(key)) {
|
||||
if (!hasSubtleCrypto) {
|
||||
@ -142,7 +140,7 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
async function makeRandomKey() {
|
||||
await generateNewLogin(publisher);
|
||||
await generateNewLogin();
|
||||
navigate("/new");
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ const Extensions = () => {
|
||||
};
|
||||
|
||||
export default function NewUserFlow() {
|
||||
const { publicKey, privateKey, generatedEntropy } = useLogin();
|
||||
const { publicKey, generatedEntropy } = useLogin();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
@ -87,10 +87,6 @@ export default function NewUserFlow() {
|
||||
<FormattedMessage {...messages.YourPubkey} />
|
||||
</h2>
|
||||
<Copy text={hexToBech32("npub", publicKey ?? "")} />
|
||||
<h2>
|
||||
<FormattedMessage {...messages.YourPrivkey} />
|
||||
</h2>
|
||||
<Copy text={hexToBech32("nsec", privateKey ?? "")} />
|
||||
<h2>
|
||||
<FormattedMessage {...messages.YourMnemonic} />
|
||||
</h2>
|
||||
|
@ -14,9 +14,8 @@ export default function NewUserName() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onNext = async () => {
|
||||
if (username.length > 0) {
|
||||
if (username.length > 0 && publisher) {
|
||||
const ev = await publisher.metadata({ name: username });
|
||||
console.debug(ev);
|
||||
publisher.broadcast(ev);
|
||||
}
|
||||
navigate("/new/verify");
|
||||
|
@ -76,13 +76,14 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
delete userCopy["zapService"];
|
||||
console.debug(userCopy);
|
||||
|
||||
const ev = await publisher.metadata(userCopy);
|
||||
console.debug(ev);
|
||||
publisher.broadcast(ev);
|
||||
if (publisher) {
|
||||
const ev = await publisher.metadata(userCopy);
|
||||
publisher.broadcast(ev);
|
||||
|
||||
const newProfile = mapEventToProfile(ev as TaggedRawEvent);
|
||||
if (newProfile) {
|
||||
await UserCache.set(newProfile);
|
||||
const newProfile = mapEventToProfile(ev as TaggedRawEvent);
|
||||
if (newProfile) {
|
||||
await UserCache.set(newProfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,11 +5,10 @@ import { randomSample, unixNowMs } from "Util";
|
||||
import Relay from "Element/Relay";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { System } from "System";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setRelays } from "Login";
|
||||
|
||||
import messages from "./messages";
|
||||
const RelaySettingsPage = () => {
|
||||
const publisher = useEventPublisher();
|
||||
const login = useLogin();
|
||||
@ -21,16 +20,18 @@ const RelaySettingsPage = () => {
|
||||
}, [relays]);
|
||||
|
||||
async function saveRelays() {
|
||||
const ev = await publisher.saveRelays();
|
||||
publisher.broadcast(ev);
|
||||
publisher.broadcastForBootstrap(ev);
|
||||
try {
|
||||
const onlineRelays = await fetch("https://api.nostr.watch/v1/online").then(r => r.json());
|
||||
const settingsEv = await publisher.saveRelaysSettings();
|
||||
const rs = Object.keys(relays).concat(randomSample(onlineRelays, 20));
|
||||
publisher.broadcastAll(settingsEv, rs);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (publisher) {
|
||||
const ev = await publisher.contactList(login.follows.item, login.relays.item);
|
||||
publisher.broadcast(ev);
|
||||
publisher.broadcastForBootstrap(ev);
|
||||
try {
|
||||
const onlineRelays = await fetch("https://api.nostr.watch/v1/online").then(r => r.json());
|
||||
const relayList = await publisher.relayList(login.relays.item);
|
||||
const rs = Object.keys(relays).concat(randomSample(onlineRelays, 20));
|
||||
publisher.broadcastAll(relayList, rs);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,12 +10,13 @@ import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
|
||||
export default function LNForwardAddress({ handle }: { handle: ManageHandle }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`);
|
||||
|
||||
const [newAddress, setNewAddress] = useState(handle.lnAddress ?? "");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function startUpdate() {
|
||||
if (!publisher) return;
|
||||
|
||||
const req = {
|
||||
lnAddress: newAddress,
|
||||
};
|
||||
@ -33,6 +34,7 @@ export default function LNForwardAddress({ handle }: { handle: ManageHandle }) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`);
|
||||
const rsp = await sp.patch(handle.id, req);
|
||||
if ("error" in rsp) {
|
||||
setError(rsp.error);
|
||||
|
@ -10,13 +10,14 @@ export default function ListHandles() {
|
||||
const navigate = useNavigate();
|
||||
const publisher = useEventPublisher();
|
||||
const [handles, setHandles] = useState<Array<ManageHandle>>([]);
|
||||
const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`);
|
||||
|
||||
useEffect(() => {
|
||||
loadHandles().catch(console.error);
|
||||
}, []);
|
||||
}, [publisher]);
|
||||
|
||||
async function loadHandles() {
|
||||
if (!publisher) return;
|
||||
const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`);
|
||||
const list = await sp.list();
|
||||
setHandles(list as Array<ManageHandle>);
|
||||
}
|
||||
|
@ -11,13 +11,13 @@ export default function TransferHandle({ handle }: { handle: ManageHandle }) {
|
||||
const publisher = useEventPublisher();
|
||||
const navigate = useNavigate();
|
||||
const { formatMessage } = useIntl();
|
||||
const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`);
|
||||
|
||||
const [newKey, setNewKey] = useState("");
|
||||
const [error, setError] = useState<Array<string>>([]);
|
||||
|
||||
async function startTransfer() {
|
||||
if (!newKey) return;
|
||||
if (!newKey || !publisher) return;
|
||||
const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`);
|
||||
setError([]);
|
||||
const rsp = await sp.transfer(handle.id, newKey);
|
||||
if ("error" in rsp) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { EventKind } from "@snort/nostr";
|
||||
import { ApiHost } from "Const";
|
||||
import { EventPublisher } from "Feed/EventPublisher";
|
||||
import { SubscriptionType } from "Subscription";
|
||||
import { EventPublisher } from "System/EventPublisher";
|
||||
|
||||
export interface RevenueToday {
|
||||
donations: number;
|
||||
@ -61,10 +62,12 @@ export default class SnortApi {
|
||||
if (!this.#publisher) {
|
||||
throw new Error("Publisher not set");
|
||||
}
|
||||
const auth = await this.#publisher.generic("", 27_235, [
|
||||
["url", `${this.#url}${path}`],
|
||||
["method", method ?? "GET"],
|
||||
]);
|
||||
const auth = await this.#publisher.generic(eb => {
|
||||
eb.kind(EventKind.HttpAuthentication);
|
||||
eb.tag(["url", `${this.#url}${path}`]);
|
||||
eb.tag(["method", method ?? "GET"]);
|
||||
return eb;
|
||||
});
|
||||
if (!auth) {
|
||||
throw new Error("Failed to create auth event");
|
||||
}
|
||||
|
101
packages/app/src/System/EventBuilder.ts
Normal file
101
packages/app/src/System/EventBuilder.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { EventKind, HexKey, NostrPrefix, RawEvent } from "@snort/nostr";
|
||||
import { HashtagRegex } from "Const";
|
||||
import { parseNostrLink, unixNow } from "Util";
|
||||
import { EventExt } from "./EventExt";
|
||||
|
||||
export class EventBuilder {
|
||||
#kind?: EventKind;
|
||||
#content?: string;
|
||||
#createdAt?: number;
|
||||
#pubkey?: string;
|
||||
#tags: Array<Array<string>> = [];
|
||||
|
||||
kind(k: EventKind) {
|
||||
this.#kind = k;
|
||||
return this;
|
||||
}
|
||||
|
||||
content(c: string) {
|
||||
this.#content = c;
|
||||
return this;
|
||||
}
|
||||
|
||||
createdAt(n: number) {
|
||||
this.#createdAt = n;
|
||||
return this;
|
||||
}
|
||||
|
||||
pubKey(k: string) {
|
||||
this.#pubkey = k;
|
||||
return this;
|
||||
}
|
||||
|
||||
tag(t: Array<string>) {
|
||||
this.#tags.push(t);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract mentions
|
||||
*/
|
||||
processContent() {
|
||||
if (this.#content) {
|
||||
this.#content = this.#content
|
||||
.replace(/@n[pub|profile|event|ote|addr|]1[acdefghjklmnpqrstuvwxyz023456789]+/g, m => this.#replaceMention(m))
|
||||
.replace(HashtagRegex, m => this.#replaceHashtag(m));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
this.#validate();
|
||||
const ev = {
|
||||
id: "",
|
||||
pubkey: this.#pubkey ?? "",
|
||||
content: this.#content ?? "",
|
||||
kind: this.#kind,
|
||||
created_at: this.#createdAt ?? unixNow(),
|
||||
tags: this.#tags,
|
||||
} as RawEvent;
|
||||
ev.id = EventExt.createId(ev);
|
||||
return ev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and sign event
|
||||
* @param pk Private key to sign event with
|
||||
*/
|
||||
async buildAndSign(pk: HexKey) {
|
||||
const ev = this.build();
|
||||
await EventExt.sign(ev, pk);
|
||||
return ev;
|
||||
}
|
||||
|
||||
#validate() {
|
||||
if (!this.#kind) {
|
||||
throw new Error("Kind must be set");
|
||||
}
|
||||
if (!this.#pubkey) {
|
||||
throw new Error("Pubkey must be set");
|
||||
}
|
||||
}
|
||||
|
||||
#replaceMention(match: string) {
|
||||
const npub = match.slice(1);
|
||||
const link = parseNostrLink(npub);
|
||||
if (link) {
|
||||
if (link.type === NostrPrefix.Profile || link.type === NostrPrefix.PublicKey) {
|
||||
this.tag(["p", link.id]);
|
||||
}
|
||||
return `nostr:${link.encode()}`;
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
#replaceHashtag(match: string) {
|
||||
const tag = match.slice(1);
|
||||
this.tag(["t", tag.toLowerCase()]);
|
||||
return match;
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ export abstract class EventExt {
|
||||
* Sign this message with a private key
|
||||
*/
|
||||
static async sign(e: RawEvent, key: HexKey) {
|
||||
e.id = await this.createId(e);
|
||||
e.id = this.createId(e);
|
||||
|
||||
const sig = await secp.schnorr.sign(e.id, key);
|
||||
e.sig = secp.utils.bytesToHex(sig);
|
||||
@ -40,12 +40,12 @@ export abstract class EventExt {
|
||||
* @returns True if valid signature
|
||||
*/
|
||||
static async verify(e: RawEvent) {
|
||||
const id = await this.createId(e);
|
||||
const id = this.createId(e);
|
||||
const result = await secp.schnorr.verify(e.sig, id, e.pubkey);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async createId(e: RawEvent) {
|
||||
static createId(e: RawEvent) {
|
||||
const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content];
|
||||
|
||||
const hash = sha256(JSON.stringify(payload));
|
||||
|
346
packages/app/src/System/EventPublisher.ts
Normal file
346
packages/app/src/System/EventPublisher.ts
Normal file
@ -0,0 +1,346 @@
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import {
|
||||
EventKind,
|
||||
FullRelaySettings,
|
||||
HexKey,
|
||||
Lists,
|
||||
RawEvent,
|
||||
RelaySettings,
|
||||
TaggedRawEvent,
|
||||
u256,
|
||||
UserMetadata,
|
||||
} from "@snort/nostr";
|
||||
|
||||
import { DefaultRelays } from "Const";
|
||||
import { System } from "System";
|
||||
import { unwrap } from "Util";
|
||||
import { EventBuilder } from "./EventBuilder";
|
||||
import { EventExt } from "./EventExt";
|
||||
|
||||
declare global {
|
||||
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>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Nip7QueueItem {
|
||||
next: () => Promise<unknown>;
|
||||
resolve(v: unknown): void;
|
||||
reject(e: unknown): void;
|
||||
}
|
||||
|
||||
const Nip7QueueDelay = 200;
|
||||
const Nip7Queue: Array<Nip7QueueItem> = [];
|
||||
async function processQueue() {
|
||||
while (Nip7Queue.length > 0) {
|
||||
const v = Nip7Queue.shift();
|
||||
if (v) {
|
||||
try {
|
||||
const ret = await v.next();
|
||||
v.resolve(ret);
|
||||
} catch (e) {
|
||||
v.reject(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(processQueue, Nip7QueueDelay);
|
||||
}
|
||||
processQueue();
|
||||
|
||||
export const barrierNip07 = async <T>(then: () => Promise<T>): Promise<T> => {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
Nip7Queue.push({
|
||||
next: then,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export type EventBuilderHook = (ev: EventBuilder) => EventBuilder;
|
||||
|
||||
export class EventPublisher {
|
||||
#pubKey: string;
|
||||
#privateKey?: string;
|
||||
#hasNip07 = "nostr" in window;
|
||||
|
||||
constructor(pubKey: string, privKey?: string) {
|
||||
if (privKey) {
|
||||
this.#privateKey = privKey;
|
||||
this.#pubKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(privKey));
|
||||
} else {
|
||||
this.#pubKey = pubKey;
|
||||
}
|
||||
}
|
||||
|
||||
#eb(k: EventKind) {
|
||||
const eb = new EventBuilder();
|
||||
return eb.pubKey(this.#pubKey).kind(k);
|
||||
}
|
||||
|
||||
async #sign(eb: EventBuilder) {
|
||||
if (this.#hasNip07 && !this.#privateKey) {
|
||||
const nip7PubKey = await barrierNip07(() => window.nostr.getPublicKey());
|
||||
if (nip7PubKey !== this.#pubKey) {
|
||||
throw new Error("Can't sign event, NIP-07 pubkey does not match");
|
||||
}
|
||||
const ev = eb.build();
|
||||
return await barrierNip07(() => window.nostr.signEvent(ev));
|
||||
} else if (this.#privateKey) {
|
||||
return await eb.buildAndSign(this.#privateKey);
|
||||
} else {
|
||||
throw new Error("Can't sign event, no private keys available");
|
||||
}
|
||||
}
|
||||
|
||||
async nip4Encrypt(content: string, key: HexKey) {
|
||||
if (this.#hasNip07 && !this.#privateKey) {
|
||||
const nip7PubKey = await barrierNip07(() => window.nostr.getPublicKey());
|
||||
if (nip7PubKey !== this.#pubKey) {
|
||||
throw new Error("Can't encrypt content, NIP-07 pubkey does not match");
|
||||
}
|
||||
return await barrierNip07(() => window.nostr.nip04.encrypt(key, content));
|
||||
} else if (this.#privateKey) {
|
||||
return await EventExt.encryptData(content, key, this.#privateKey);
|
||||
} else {
|
||||
throw new Error("Can't encrypt content, no private keys available");
|
||||
}
|
||||
}
|
||||
|
||||
async nip4Decrypt(content: string, otherKey: HexKey) {
|
||||
if (this.#hasNip07 && !this.#privateKey) {
|
||||
return await barrierNip07(() => window.nostr.nip04.decrypt(otherKey, content));
|
||||
} else if (this.#privateKey) {
|
||||
return await EventExt.decryptDm(content, this.#privateKey, otherKey);
|
||||
} else {
|
||||
throw new Error("Can't decrypt content, no private keys available");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
broadcast(ev: RawEvent) {
|
||||
console.debug(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: RawEvent) {
|
||||
for (const [k] of DefaultRelays) {
|
||||
System.WriteOnceToRelay(k, ev);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write event to all given relays.
|
||||
*/
|
||||
broadcastAll(ev: RawEvent, relays: string[]) {
|
||||
for (const k of relays) {
|
||||
System.WriteOnceToRelay(k, ev);
|
||||
}
|
||||
}
|
||||
|
||||
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: TaggedRawEvent, 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?.Event ?? thread.replyTo?.Event ?? "", "", "root"]);
|
||||
}
|
||||
eb.tag(["e", replyTo.id, replyTo.relays[0] ?? "", "reply"]);
|
||||
|
||||
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: RawEvent, 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");
|
||||
}
|
||||
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: RawEvent) {
|
||||
const eb = this.#eb(EventKind.Repost);
|
||||
eb.tag(["e", note.id, ""]);
|
||||
eb.tag(["p", note.pubkey]);
|
||||
return await this.#sign(eb);
|
||||
}
|
||||
|
||||
async decryptDm(note: RawEvent) {
|
||||
if (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();
|
||||
fnHook(eb);
|
||||
return await this.#sign(eb);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import { AuthHandler, TaggedRawEvent, RelaySettings, Connection, RawReqFilter, R
|
||||
|
||||
import { sanitizeRelayUrl, unixNowMs, unwrap } from "Util";
|
||||
import { RequestBuilder } from "./RequestBuilder";
|
||||
import { EventBuilder } from "./EventBuilder";
|
||||
import {
|
||||
FlatNoteStore,
|
||||
NoteStore,
|
||||
@ -18,6 +19,7 @@ export {
|
||||
PubkeyReplaceableNoteStore,
|
||||
ParameterizedReplaceableNoteStore,
|
||||
Query,
|
||||
EventBuilder,
|
||||
};
|
||||
|
||||
export interface SystemSnapshot {
|
||||
|
@ -23,11 +23,9 @@ export function hexToMnemonic(hex: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert mnemonic phrase into hex-encoded private key
|
||||
* using the derivation path specified in NIP06
|
||||
* @param mnemonic the mnemonic-encoded entropy
|
||||
* Derrive NIP-06 private key from master key
|
||||
*/
|
||||
export function entropyToDerivedKey(entropy: Uint8Array): string {
|
||||
export function entropyToPrivateKey(entropy: Uint8Array): string {
|
||||
const masterKey = HDKey.fromMasterSeed(entropy);
|
||||
const newKey = masterKey.derive(DerivationPath);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user