adding stuff

This commit is contained in:
nate
2024-06-12 00:30:26 -04:00
parent 7064002fa8
commit 187768bbf2
15 changed files with 753 additions and 12 deletions

View File

@@ -0,0 +1,15 @@
export enum NDKKind {
Highlight = 9802,
UserList = 30000,
GenericList = 30001,
HighlightList = 39802,
RelayList = 30022,
LongForm = 30023,
}
export const NDKListKinds = [
NDKKind.UserList,
NDKKind.GenericList,
NDKKind.RelayList,
NDKKind.HighlightList,
];

View File

@@ -0,0 +1,184 @@
import NDK, { NDKEvent, NDKUser, type NDKTag, type NostrEvent, NDKRelay, type NDKSigner, type NDKFilter, mergeFilters } from '@nostr-dev-kit/ndk';
import { NDKKind } from '../index';
import { filterForId } from '$lib/utils/index';
/**
* Represents any NIP-33 list kind.
*/
class NDKList extends NDKEvent {
public _encryptedTags: NDKTag[] | undefined;
/**
* Stores the number of bytes the content was before decryption
* to expire the cache when the content changes.
*/
private encryptedTagsLength: number | undefined;
constructor(ndk?: NDK, rawEvent?: NostrEvent) {
super(ndk, rawEvent);
if (!this.kind) this.kind = NDKKind.GenericList;
}
static from(ndkEvent: NDKEvent): NDKList {
return new NDKList(ndkEvent.ndk, ndkEvent.rawEvent());
}
get name(): string | undefined {
return this.tagValue('name') ?? this.tagValue('d');
}
set name(name: string | undefined) {
this.removeTag('name');
if (name) {
this.tags.push(['name', name]);
} else {
throw new Error('Name cannot be empty');
}
}
get description(): string | undefined {
return this.tagValue('description');
}
set description(name: string | undefined) {
if (name) {
this.tags.push(['description', name]);
} else {
this.removeTag('description');
}
}
/**
* Returns the decrypted content of the list.
*/
async encryptedTags(useCache = true): Promise<NDKTag[]> {
if (this._encryptedTags && this.encryptedTagsLength === this.content.length && useCache) return this._encryptedTags;
if (!this.ndk) throw new Error('NDK instance not set');
if (!this.ndk.signer) throw new Error('NDK signer not set');
const user = await this.ndk.signer.user();
try {
if (this.content.length > 0) {
try {
console.log(`decrypting ${this.content}`);
const decryptedContent = await this.ndk.signer.decrypt(user, this.content);
const a = JSON.parse(decryptedContent);
if (a && a[0]) {
this.encryptedTagsLength = this.content.length;
return this._encryptedTags = a;
}
this.encryptedTagsLength = this.content.length;
return this._encryptedTags = [];
} catch (e) {
console.log(`error decrypting ${this.content}`);
}
}
} catch (e) {
// console.trace(e);
// throw e;
}
return [];
}
public validateTag(tagValue: string): boolean | string {
return true;
}
get items(): NDKTag[] {
return this.tags.filter((t) => {
return !['d', 'name', 'description'].includes(t[0]);
});
}
async fetchItems(): Promise<Set<NDKEvent>> {
let filters: NDKFilter[] = []
if (!this.ndk) throw new Error('NDK instance not set');
filters = this.items.map(i => filterForId(i[1]));
console.log(`filters: ${JSON.stringify(filters)}`);
console.log(mergeFilters(filters));
return this.ndk.fetchEvents(
mergeFilters(filters)
);
}
async addItem(relay: NDKRelay, mark?: string, encrypted?: boolean): Promise<void>;
async addItem(event: NDKEvent, mark?: string, encrypted?: boolean): Promise<void>;
async addItem(user: NDKUser, mark?: string, encrypted?: boolean): Promise<void>;
async addItem(tag: NDKTag, mark?: string, encrypted?: boolean): Promise<void>;
async addItem(obj: NDKUser | NDKEvent | NDKRelay | NDKTag, mark: string | undefined = undefined, encrypted: boolean = false): Promise<void> {
if (!this.ndk) throw new Error('NDK instance not set');
if (!this.ndk.signer) throw new Error('NDK signer not set');
let tag;
if (obj instanceof NDKEvent) {
tag = obj.tagReference();
} else if (obj instanceof NDKUser) {
tag = obj.tagReference();
} else if (obj instanceof NDKRelay) {
tag = ['r', (obj as NDKRelay).url];
} else if (Array.isArray(obj)) {
// NDKTag
tag = obj;
} else {
throw new Error('Invalid object type');
}
if (mark) tag.push(mark);
if (encrypted) {
const user = await this.ndk.signer.user();
const currentList = await this.encryptedTags();
console.log(`current list: ${JSON.stringify(currentList)}`)
currentList.push(tag);
this._encryptedTags = currentList;
this.encryptedTagsLength = this.content.length;
this.content = JSON.stringify(currentList);
await this.encrypt(user);
} else {
this.tags.push(tag);
}
this.created_at = Math.floor(Date.now() / 1000);
}
/**
* Removes an item from the list.
*
* @param index The index of the item to remove.
* @param encrypted Whether to remove from the encrypted list or not.
*/
async removeItem(index: number, encrypted: boolean): Promise<NDKList> {
if (!this.ndk) throw new Error('NDK instance not set');
if (!this.ndk.signer) throw new Error('NDK signer not set');
if (encrypted) {
const user = await this.ndk.signer.user();
const currentList = await this.encryptedTags();
currentList.splice(index, 1);
this._encryptedTags = currentList;
this.encryptedTagsLength = this.content.length;
this.content = JSON.stringify(currentList);
await this.encrypt(user);
} else {
this.tags.splice(index, 1);
}
this.created_at = Math.floor(Date.now() / 1000);
return this;
}
}
export default NDKList;

View File

@@ -0,0 +1,31 @@
import type { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk';
import NDKList from './index.js';
import type NDK from '@nostr-dev-kit/ndk';
class NDKRelayList extends NDKList {
constructor(ndk?: NDK, rawEvent?: NostrEvent) {
super(ndk, rawEvent);
}
static from(ndkEvent: NDKEvent) {
return new NDKRelayList(ndkEvent.ndk, ndkEvent.rawEvent());
}
public validateTag(tagValue: string): boolean | string {
if (tagValue.startsWith('wss://')) return true;
if (tagValue.startsWith('nrelay')) return true;
return 'Invalid relay URL';
}
/**
* Get the list of relays
*/
get relays(): string[] {
console.log(this.getMatchingTags('r'));
return this.getMatchingTags('r').map((tag) => tag[1]);
}
}
export default NDKRelayList;

View File

@@ -0,0 +1,142 @@
import NDK, { NDKEvent, NDKPrivateKeySigner, type NDKFilter, type NDKSigner, type NDKUserProfile } from "@nostr-dev-kit/ndk";
import type { NDKTag, NostrEvent} from "@nostr-dev-kit/ndk";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex } from "@noble/hashes/utils";
const CURRENT_PRIVATE_NOTE_VERSION = '2';
interface IFindEphemeralSignerLookups {
name?: string,
associatedEventNip19?: string;
}
/**
* Find a named ephemeral signer from a self-DM.
*/
export async function findEphemeralSigner(
ndk: NDK,
mainSigner: NDKSigner,
opts: IFindEphemeralSignerLookups
): Promise<NDKPrivateKeySigner | undefined> {
const mainUser = await mainSigner.user();
const filter: NDKFilter = { kinds: [2600 as number]};
if (opts.name) {
const hashedName = await getHashedKeyName(opts.name);
filter['#e'] = [hashedName];
} else if (opts.associatedEventNip19) {
const hashedEventReference = await getHashedKeyName(opts.associatedEventNip19);
filter['#e'] = [hashedEventReference];
}
const event = await ndk.fetchEvent(filter);
if (event) {
const decryptEventFunction = async (event: NDKEvent) => {
await event.decrypt(await mainSigner.user());
const content = JSON.parse(event.content);
return new NDKPrivateKeySigner(content.key);
};
const promise = new Promise<NDKPrivateKeySigner>((resolve, reject) => {
let decryptionAttempts = 0;
try {
decryptionAttempts++;
resolve(decryptEventFunction(event));
} catch(e) {
if (decryptionAttempts> 5) {
console.error(`Failed to decrypt ephemeral signer event after ${decryptionAttempts} attempts`);
reject(e);
return;
}
setTimeout(() => { decryptEventFunction(event);}, 1000 * Math.random());
}
});
return promise;
}
}
type EphemeralKeyEventContent = {
key: string;
event?: string;
version: string;
metadata?: object;
}
interface ISaveOpts {
associatedEvent?: NDKEvent;
name?: string;
metadata?: object;
keyProfile?: NDKUserProfile;
mainSigner?: NDKSigner;
}
function generateContent(targetSigner: NDKPrivateKeySigner, opts: ISaveOpts = {}) {
const content: EphemeralKeyEventContent = {
key: targetSigner.privateKey!,
version: CURRENT_PRIVATE_NOTE_VERSION,
...opts.metadata,
};
if (opts.associatedEvent) content.event = opts.associatedEvent.encode();
return JSON.stringify(content);
}
async function getHashedKeyName(name:string) {
let eventHash = sha256(name);
return bytesToHex(eventHash);
}
async function generateTags(mainSigner:NDKSigner, opts: ISaveOpts = {}) {
const mainUser = await mainSigner.user();
const tags = [
['p', mainUser.pubkey],
['client', 'cofabricate'],
]
if (opts.associatedEvent) {
const hashedEventReference = await getHashedKeyName(opts.associatedEvent.encode());
tags.push(['e', hashedEventReference]);
}
if (opts.name) {
const hashedName = await getHashedKeyName(opts.name);
tags.push(['e', hashedName]);
}
return tags;
}
export async function saveEphemeralSigner(ndk: NDK, targetSigner: NDKPrivateKeySigner, opts: ISaveOpts = {}) {
const mainSigner = opts.mainSigner || ndk.signer;
if (!mainSigner) throw new Error('No main signer provided');
const mainUser = await mainSigner.user();
const event = new NDKEvent(ndk, {
kind: 2600,
content: generateContent(targetSigner, opts),
tags: await generateTags(mainSigner, opts),
} as NostrEvent);
event.pubkey = mainUser.pubkey;
await event.encrypt(mainUser, mainSigner);
await event.publish();
if (opts.keyProfile) {
const user = await targetSigner.user();
const event = new NDKEvent(ndk, {
kind: 0,
content: JSON.stringify(opts.keyProfile),
tags: [] as NDKTag[],
} as NostrEvent)
event.pubkey = user.pubkey;
await event.sign(targetSigner);
await event.publish();
}
}
export function generateEphemeralSigner(): NDKPrivateKeySigner {
const signer = NDKPrivateKeySigner.generate();
return signer;
}

24
src/lib/store.ts Normal file
View File

@@ -0,0 +1,24 @@
import { writable } from "svelte/store";
import { NDKUser } from "@nostr-dev-kit/ndk";
export const currentUser = writable<NDKUser | null>(null);
export const currentUserFollowPubkeys = writable<string[] | undefined>(undefined);
export const backgroundBanner = writable<string | null>(null);
export type ScopeSelection = {
label: string;
id: string;
pubkeys: string[] | undefined;
};
export const currentScope = writable<ScopeSelection>({
label: 'global',
id: 'global',
pubkeys: undefined,
});
let zapEvent: any;
export const zap = writable(zapEvent);
export const pageTitle = writable<string | null>(null);
export const pageSubtitle = writable<string | null>(null);

35
src/lib/stores/nostr.ts Normal file
View File

@@ -0,0 +1,35 @@
import { writable } from "svelte/store";
import NDK from "@nostr-dev-kit/ndk";
import NDKSvelte from "@nostr-dev-kit/ndk-svelte";
import { RelayList } from "@nostr-dev-kit/ndk-svelte-components";
let relays;
try {
relays = localStorage.getItem('relays');
} catch (e) {}
let relayList: string[] = [];
if (relays) {
relayList = JSON.parse(relays);
}
export const defaultRelays = [
'wss://purplepag.es',
'wss://relay.damus.io'
]
if (!relayList || !Array.isArray(relayList) || relayList.length === 0) {
relayList = defaultRelays;
}
const _ndk: NDKSvelte = new NDKSvelte({
devWriteRelayUrls: ['wss://relay.strfront.com'],
explicitRelayUrls: relayList,
enableOutboxModel: true,
}) as NDKSvelte;
const ndk = writable(_ndk);
export default ndk;

82
src/lib/stores/signer.ts Normal file
View File

@@ -0,0 +1,82 @@
import { findEphemeralSigner } from "$lib/signers/ephemeral";
import { NDKPrivateKeySigner, type NDKSigner, type NDKUser } from "@nostr-dev-kit/ndk";
import { writable, get as getStore, derived } from "svelte/store";
import ndkStore from "./nostr";
import { currentUser as currentUserStore } from "../store";
import type NDKList from "$lib/ndk-kinds/lists";
export type SignerStoreItem = {
signer: NDKPrivateKeySigner;
user: NDKUser;
saved: boolean;
name?: string;
id: string;
};
type SignerItems = Map<string, SignerStoreItem>;
export const signers = writable<SignerItems>(new Map());
export const npubSigners = derived(signers, ($signers) => {
const npubs = new Map<string, NDKSigner>();
for (const entry of $signers) {
const { user, signer } = entry[1];
npubs.set(user.npub, signer);
}
return npubs;
});
async function getDelegatedSignerName(list: NDKList) {
let name = '';
const currentUser: NDKUser = getStore(currentUserStore);
if (!currentUser?.profile) {
currentUser.ndk = getStore(ndkStore);
await currentUser?.fetchProfile();
}
if (currentUser?.profile?.name) {
name = currentUser.profile.displayName + `'s `;
}
return name + list.name;
}
export async function getSigner(list: NDKList): Promise<SignerStoreItem> {
const store = getStore(signers);
const id = list.encode();
let item = store.get(id);
if (item) return item;
const ndk = getStore(ndkStore);
let signer = await findEphemeralSigner(ndk, ndk.signer!, {
associatedEventNip19: list.encode(),
});
if (signer) {
console.log(`found a signer for list ${list.name}`);
item = {
signer: signer!,
user: await signer.user(),
saved: true,
id,
};
} else {
signer = NDKPrivateKeySigner.generate();
item = {
signer,
user: await signer.user(),
saved: false,
name: await getDelegatedSignerName(list),
id,
};
}
item.user.ndk = ndk;
store.set(id, item);
return item;
}

190
src/lib/utils/index.ts Normal file
View File

@@ -0,0 +1,190 @@
import sanitizeHtml from 'sanitize-html';
import MarkdownIt from 'markdown-it';
import { nip19 } from 'nostr-tools';
import { filterFromId, type NDKFilter, type NDKTag } from '@nostr-dev-kit/ndk';
export function tagToNip19(tag: NDKTag): string | undefined {
switch (tag[0]) {
case 'a':
const [ kind, pubkey, identifier ] = tag[1].split(':');
return nip19.naddrEncode({
kind: parseInt(kind),
pubkey,
identifier
})
case 'e':
return nip19.noteEncode(tag[1]);
case 'r':
return tag[1];
}
}
export function nicelyFormattedMilliSatNumber(amount: number) {
return nicelyFormattedSatNumber(
Math.floor(amount / 1000)
);
}
export function nicelyFormattedSatNumber(amount: number) {
// if the number is less than 1000, just return it
if (amount < 1000) return amount;
// if the number is less than 1 million, return it with a k, if the comma is not needed remove it
if (amount < 1000000) return `${(amount / 1000).toFixed(0)}k`;
// if the number is less than 1 billion, return it with an m
if (amount < 1000000000) return `${(amount / 1000000).toFixed(1)}m`;
return `${(amount / 100_000_000).toFixed(2)} btc`;
}
export function filterForId(id: string): NDKFilter {
if (!!id.match(/:/)) {
const [kind, pubkey, identifier] = id.split(':');
return { kinds: [parseInt(kind)], authors: [pubkey], '#d': [identifier] };
} else {
return { ids: [id] };
}
}
export function filterFromNaddr(naddr: string): NDKFilter {
return filterFromId(naddr);
}
export function idFromNaddr(naddr: string) {
const ndecode = nip19.decode(naddr).data as any;
return `${ndecode.kind}:${ndecode.pubkey}:${ndecode.identifier}`;
}
export function naddrFromTagValue(value: string) {
const [kind, pubkey, identifier] = value.split(':');
return nip19.naddrEncode({
kind: parseInt(kind),
pubkey,
identifier
});
}
export function prettifyContent(content: string) {
const bitcoinImage =
'<img src="https://abs.twimg.com/hashflags/Bitcoin_evergreen/Bitcoin_evergreen.png" style="width: 1.2em; vertical-align: -20%; margin-right: 0.075em; height: 1.2em; margin-left: 2px; display: inline-block;">';
content = content.replace(/#bitcoin/i, `#bitcoin${bitcoinImage}`);
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
autolink: true,
image: true,
});
md.linkify?.set();
content = md.render(content);
return sanitizeHtml(content);
}
function flattenText(node: string) {
const texts = [];
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
while (walker.nextNode()) {
texts.push(walker.currentNode);
}
return texts;
}
export function highlightText(targetText: string, highlightId: string) {
const regex = new RegExp(escapeRegExp(targetText), 'g');
const textNodes = flattenText(document.body);
const marks: HTMLElement[] = [];
textNodes.forEach((textNode) => {
let match;
let lastIndex = 0;
while ((match = regex.exec(textNode.data)) !== null) {
const mark = document.createElement('mark');
mark.setAttribute('data-highlight-id', highlightId);
const range = document.createRange();
const startOffset = match.index;
const endOffset = startOffset + targetText.length;
if (lastIndex < startOffset) {
const precedingTextNode = document.createTextNode(textNode.data.slice(lastIndex, startOffset));
textNode.parentNode.insertBefore(precedingTextNode, textNode);
}
range.setStart(textNode, startOffset);
range.setEnd(textNode, endOffset);
range.surroundContents(mark);
marks.push(mark);
lastIndex = endOffset;
}
if (lastIndex < textNode.length) {
const remainingTextNode = document.createTextNode(textNode.data.slice(lastIndex));
textNode.parentNode.insertBefore(remainingTextNode, textNode);
}
textNode.remove();
});
return marks;
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
}
// Usage example: highlightText("your target text here");
// export function modifyDocument(content: string) {
// const regex = new RegExp(content, 'gi');
// // get all the text nodes in the current page
// const textNodes = document.evaluate("//text()", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
// // loop through all the text nodes and replace the search string with the marked up version
// for (let i = 0; i < textNodes.snapshotLength; i++) {
// const node = textNodes.snapshotItem(i) as Text;
// const parent = node.parentNode;
// // check if the text node contains the search string
// if (!node || !node.textContent || !node.textContent.match(regex)) {
// continue;
// }
// const fragment = document.createDocumentFragment();
// let match;
// if (!node) continue;
// // loop through all the matches in the text node
// while ((match = regex.exec(node.textContent)) !== null) {
// console.log({match, node});
// const before = node.splitText(match.index);
// const after = node.splitText(match[0].length);
// console.log({before, after});
// const mark = document.createElement("mark");
// // append the matched text to the mark element
// mark.appendChild(document.createTextNode(match[0]));
// // append the mark element to the document fragment
// fragment.appendChild(before);
// fragment.appendChild(mark);
// node = after;
// }
// // replace the original text node with the marked up version
// parent.replaceChild(fragment, node);
// }
// }