chore: monorepo

This commit is contained in:
2023-12-25 14:28:39 +07:00
parent a6da07cd3f
commit 227c2ddefa
374 changed files with 19966 additions and 12758 deletions

543
packages/ark/src/ark.ts Normal file
View File

@@ -0,0 +1,543 @@
import { LumeStorage } from "@lume/storage";
import {
type Account,
type NDKEventWithReplies,
type NIP05,
} from "@lume/types";
import NDK, {
NDKEvent,
NDKFilter,
NDKKind,
NDKNip46Signer,
NDKPrivateKeySigner,
NDKRelay,
NDKSubscriptionCacheUsage,
NDKTag,
NDKUser,
NostrEvent,
} from "@nostr-dev-kit/ndk";
import { open } from "@tauri-apps/plugin-dialog";
import { readBinaryFile } from "@tauri-apps/plugin-fs";
import { fetch } from "@tauri-apps/plugin-http";
import { NostrFetcher, normalizeRelayUrl } from "nostr-fetch";
import { nip19 } from "nostr-tools";
export class Ark {
#storage: LumeStorage;
#fetcher: NostrFetcher;
public ndk: NDK;
public account: Account;
constructor({
ndk,
storage,
fetcher,
}: {
ndk: NDK;
storage: LumeStorage;
fetcher: NostrFetcher;
}) {
this.ndk = ndk;
this.#storage = storage;
this.#fetcher = fetcher;
}
public async connectDepot() {
return this.ndk.addExplicitRelay(
new NDKRelay(normalizeRelayUrl("ws://localhost:6090")),
undefined,
true,
);
}
public updateNostrSigner({
signer,
}: { signer: NDKNip46Signer | NDKPrivateKeySigner }) {
this.ndk.signer = signer;
return this.ndk.signer;
}
public subscribe({
filter,
closeOnEose = false,
cb,
}: {
filter: NDKFilter;
closeOnEose: boolean;
cb: (event: NDKEvent) => void;
}) {
const sub = this.ndk.subscribe(filter, { closeOnEose });
sub.addListener("event", (event: NDKEvent) => cb(event));
return sub;
}
public async createEvent({
kind,
tags,
content,
rootReplyTo = undefined,
replyTo = undefined,
}: {
kind: NDKKind | number;
tags: NDKTag[];
content?: string;
rootReplyTo?: string;
replyTo?: string;
}) {
try {
const event = new NDKEvent(this.ndk);
if (content) event.content = content;
event.kind = kind;
event.tags = tags;
if (rootReplyTo) {
const rootEvent = await this.ndk.fetchEvent(rootReplyTo);
if (rootEvent) event.tag(rootEvent, "root");
}
if (replyTo) {
const replyEvent = await this.ndk.fetchEvent(replyTo);
if (replyEvent) event.tag(replyEvent, "reply");
}
const publish = await event.publish();
if (!publish) throw new Error("Failed to publish event");
return {
id: event.id,
seens: [...publish.values()].map((item) => item.url),
};
} catch (e) {
throw new Error(e);
}
}
public async getUserProfile({ pubkey }: { pubkey: string }) {
try {
// get clean pubkey without any special characters
let hexstring = pubkey.replace(/[^a-zA-Z0-9]/g, "");
if (
hexstring.startsWith("npub1") ||
hexstring.startsWith("nprofile1") ||
hexstring.startsWith("naddr1")
) {
const decoded = nip19.decode(hexstring);
if (decoded.type === "nprofile") hexstring = decoded.data.pubkey;
if (decoded.type === "npub") hexstring = decoded.data;
if (decoded.type === "naddr") hexstring = decoded.data.pubkey;
}
const user = this.ndk.getUser({ pubkey: hexstring });
const profile = await user.fetchProfile({
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
});
if (!profile) return null;
return profile;
} catch (e) {
throw new Error(e);
}
}
public async getUserContacts({
pubkey = undefined,
outbox = undefined,
}: {
pubkey?: string;
outbox?: boolean;
}) {
try {
const user = this.ndk.getUser({
pubkey: pubkey ? pubkey : this.#storage.account.pubkey,
});
const contacts = [...(await user.follows(undefined, outbox))].map(
(user) => user.pubkey,
);
if (pubkey === this.#storage.account.pubkey)
this.#storage.account.contacts = contacts;
return contacts;
} catch (e) {
throw new Error(e);
}
}
public async getUserRelays({ pubkey }: { pubkey?: string }) {
try {
const user = this.ndk.getUser({
pubkey: pubkey ? pubkey : this.#storage.account.pubkey,
});
return await user.relayList();
} catch (e) {
throw new Error(e);
}
}
public async newContactList({ tags }: { tags: NDKTag[] }) {
const publish = await this.createEvent({
kind: NDKKind.Contacts,
tags: tags,
});
if (publish) {
this.#storage.account.contacts = tags.map((item) => item[1]);
return publish;
}
}
public async createContact({ pubkey }: { pubkey: string }) {
const user = this.ndk.getUser({ pubkey: this.#storage.account.pubkey });
const contacts = await user.follows();
return await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
}
public async deleteContact({ pubkey }: { pubkey: string }) {
const user = this.ndk.getUser({ pubkey: this.#storage.account.pubkey });
const contacts = await user.follows();
contacts.delete(new NDKUser({ pubkey: pubkey }));
const event = new NDKEvent(this.ndk);
event.content = "";
event.kind = NDKKind.Contacts;
event.tags = [...contacts].map((item) => [
"p",
item.pubkey,
item.relayUrls?.[0] || "",
"",
]);
return await event.publish();
}
public async getAllEvents({ filter }: { filter: NDKFilter }) {
const events = await this.ndk.fetchEvents(filter);
if (!events) return [];
return [...events];
}
public async getEventById({ id }: { id: string }) {
let eventId: string = id;
if (
eventId.startsWith("nevent1") ||
eventId.startsWith("note1") ||
eventId.startsWith("naddr1")
) {
const decode = nip19.decode(eventId);
if (decode.type === "nevent") eventId = decode.data.id;
if (decode.type === "note") eventId = decode.data;
if (decode.type === "naddr") {
return await this.ndk.fetchEvent({
kinds: [decode.data.kind],
"#d": [decode.data.identifier],
authors: [decode.data.pubkey],
});
}
}
return await this.ndk.fetchEvent(id, {
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
});
}
public async getEventByFilter({ filter }: { filter: NDKFilter }) {
const event = await this.ndk.fetchEvent(filter, {
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
});
if (!event) return null;
return event;
}
public getEventThread({ tags }: { tags: NDKTag[] }) {
let rootEventId: string = null;
let replyEventId: string = null;
const events = tags.filter((el) => el[0] === "e");
if (!events.length) return null;
if (events.length === 1)
return {
rootEventId: events[0][1],
replyEventId: null,
};
if (events.length > 1) {
rootEventId = events.find((el) => el[3] === "root")?.[1];
replyEventId = events.find((el) => el[3] === "reply")?.[1];
if (!rootEventId && !replyEventId) {
rootEventId = events[0][1];
replyEventId = events[1][1];
}
}
return {
rootEventId,
replyEventId,
};
}
public async getThreads({
id,
data,
}: { id: string; data?: NDKEventWithReplies[] }) {
let events = data || null;
if (!data) {
const relayUrls = [...this.ndk.pool.relays.values()].map(
(item) => item.url,
);
const rawEvents = (await this.#fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.Text],
"#e": [id],
},
{ since: 0 },
{ sort: true },
)) as unknown as NostrEvent[];
events = rawEvents.map(
(event) => new NDKEvent(this.ndk, event),
) as NDKEvent[] as NDKEventWithReplies[];
}
if (events.length > 0) {
const replies = new Set();
for (const event of events) {
const tags = event.tags.filter((el) => el[0] === "e" && el[1] !== id);
if (tags.length > 0) {
for (const tag of tags) {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
if (rootEvent?.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
}
replies.add(event.id);
}
}
}
}
const cleanEvents = events.filter((ev) => !replies.has(ev.id));
return cleanEvents;
}
return events;
}
public async getAllRelaysFromContacts() {
const LIMIT = 1;
const connectedRelays = this.ndk.pool
.connectedRelays()
.map((item) => item.url);
const relayMap = new Map<string, string[]>();
const relayEvents = this.#fetcher.fetchLatestEventsPerAuthor(
{
authors: this.#storage.account.contacts,
relayUrls: connectedRelays,
},
{ kinds: [NDKKind.RelayList] },
LIMIT,
);
for await (const { author, events } of relayEvents) {
if (events[0]) {
for (const tag of events[0].tags) {
const users = relayMap.get(tag[1]);
if (!users) relayMap.set(tag[1], [author]);
users.push(author);
}
}
}
return relayMap;
}
public async getInfiniteEvents({
filter,
limit,
pageParam = 0,
signal = undefined,
dedup = true,
}: {
filter: NDKFilter;
limit: number;
pageParam?: number;
signal?: AbortSignal;
dedup?: boolean;
}) {
const rootIds = new Set();
const dedupQueue = new Set();
const connectedRelays = this.ndk.pool
.connectedRelays()
.map((item) => item.url);
const events = await this.#fetcher.fetchLatestEvents(
connectedRelays,
filter,
limit,
{
asOf: pageParam === 0 ? undefined : pageParam,
abortSignal: signal,
},
);
const ndkEvents = events.map((event) => {
return new NDKEvent(this.ndk, event);
});
if (dedup) {
for (const event of ndkEvents) {
const tags = event.tags.filter((el) => el[0] === "e");
if (tags && tags.length > 0) {
const rootId = tags.filter((el) => el[3] === "root")[1] ?? tags[0][1];
if (rootIds.has(rootId)) {
dedupQueue.add(event.id);
break;
}
rootIds.add(rootId);
}
}
return ndkEvents
.filter((event) => !dedupQueue.has(event.id))
.sort((a, b) => b.created_at - a.created_at);
}
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
}
public async getRelayEvents({
relayUrl,
filter,
limit,
pageParam = 0,
signal = undefined,
}: {
relayUrl: string;
filter: NDKFilter;
limit: number;
pageParam?: number;
signal?: AbortSignal;
dedup?: boolean;
}) {
const events = await this.#fetcher.fetchLatestEvents(
[normalizeRelayUrl(relayUrl)],
filter,
limit,
{
asOf: pageParam === 0 ? undefined : pageParam,
abortSignal: signal,
},
);
const ndkEvents = events.map((event) => {
return new NDKEvent(this.ndk, event);
});
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
}
/**
* Upload media file to nostr.build
* @todo support multiple backends
*/
public async upload({ fileExts }: { fileExts?: string[] }) {
const defaultExts = ["png", "jpeg", "jpg", "gif"].concat(fileExts);
const selected = await open({
multiple: false,
filters: [
{
name: "Image",
extensions: defaultExts,
},
],
});
if (!selected) return null;
const file = await readBinaryFile(selected.path);
const blob = new Blob([file]);
const data = new FormData();
data.append("fileToUpload", blob);
data.append("submit", "Upload Image");
const res = await fetch("https://nostr.build/api/v2/upload/files", {
method: "POST",
body: data,
});
if (!res.ok) return null;
const json = await res.json();
const content = json.data[0];
return content.url as string;
}
public async validateNIP05({
pubkey,
nip05,
signal,
}: {
pubkey: string;
nip05: string;
signal?: AbortSignal;
}) {
const localPath = nip05.split("@")[0];
const service = nip05.split("@")[1];
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
const res = await fetch(verifyURL, {
method: "GET",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
signal,
});
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
const data: NIP05 = await res.json();
if (!data.names) return false;
if (data.names[localPath.toLowerCase()] === pubkey) return true;
if (data.names[localPath] === pubkey) return true;
return false;
}
public async replyTo({
content,
event,
}: { content: string; event: NDKEvent }) {
try {
const replyEvent = new NDKEvent(this.ndk);
replyEvent.content = content;
replyEvent.kind = NDKKind.Text;
replyEvent.tag(event, "reply");
return await replyEvent.publish();
} catch (e) {
throw new Error(e);
}
}
}