feat: update rust nostr
This commit is contained in:
@@ -1,301 +1,56 @@
|
||||
import { 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 { ndkAdapter } from "@nostr-fetch/adapter-ndk";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { NostrFetcher, normalizeRelayUrl } from "nostr-fetch";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { type CurrentAccount, Event, Metadata } from "@lume/types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export class Ark {
|
||||
public ndk: NDK;
|
||||
public account: Account;
|
||||
public account: CurrentAccount;
|
||||
|
||||
constructor({
|
||||
ndk,
|
||||
account,
|
||||
}: {
|
||||
ndk: NDK;
|
||||
account: Account;
|
||||
}) {
|
||||
this.ndk = ndk;
|
||||
constructor(account: CurrentAccount) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
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 getNDKEvent(event: NostrEvent) {
|
||||
return new NDKEvent(this.ndk, event);
|
||||
}
|
||||
|
||||
public async createEvent({
|
||||
kind,
|
||||
tags,
|
||||
content,
|
||||
rootReplyTo = undefined,
|
||||
replyTo = undefined,
|
||||
}: {
|
||||
kind: NDKKind | number;
|
||||
tags: NDKTag[];
|
||||
content?: string;
|
||||
rootReplyTo?: string;
|
||||
replyTo?: string;
|
||||
}) {
|
||||
public async event_to_bech32(id: string, relays: 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 getCleanPubkey(pubkey: string) {
|
||||
try {
|
||||
let hexstring = pubkey
|
||||
.replace("nostr:", "")
|
||||
.split("'")[0]
|
||||
.split(".")[0]
|
||||
.split(",")[0]
|
||||
.split("?")[0];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return hexstring;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserProfile(pubkey?: string) {
|
||||
try {
|
||||
const currentUserPubkey = this.account.pubkey;
|
||||
const hexstring = pubkey
|
||||
? this.getCleanPubkey(pubkey)
|
||||
: currentUserPubkey;
|
||||
|
||||
const user = this.ndk.getUser({
|
||||
pubkey: hexstring,
|
||||
const cmd: string = await invoke("event_to_bech32", {
|
||||
id,
|
||||
relays,
|
||||
});
|
||||
|
||||
const profile = await user.fetchProfile({
|
||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
|
||||
});
|
||||
|
||||
return profile;
|
||||
return cmd;
|
||||
} catch {
|
||||
throw new Error("user not found");
|
||||
console.error("get nevent id failed");
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserContacts(pubkey?: string) {
|
||||
public async get_event(id: string) {
|
||||
try {
|
||||
const currentUserPubkey = this.account.pubkey;
|
||||
const hexstring = pubkey
|
||||
? this.getCleanPubkey(pubkey)
|
||||
: currentUserPubkey;
|
||||
|
||||
const user = this.ndk.getUser({
|
||||
pubkey: hexstring,
|
||||
});
|
||||
|
||||
const contacts = [...(await user.follows(undefined, false))].map(
|
||||
(user) => user.pubkey,
|
||||
);
|
||||
|
||||
if (!pubkey || pubkey === this.account.pubkey) {
|
||||
this.account.contacts = contacts;
|
||||
}
|
||||
|
||||
return contacts;
|
||||
const cmd: string = await invoke("get_event", { id });
|
||||
const event = JSON.parse(cmd) as Event;
|
||||
return event;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error("failed to get event", id);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserRelays({ pubkey }: { pubkey?: string }) {
|
||||
try {
|
||||
const user = this.ndk.getUser({
|
||||
pubkey: pubkey ? pubkey : this.account.pubkey,
|
||||
});
|
||||
return await user.relayList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async newContactList({ tags }: { tags: NDKTag[] }) {
|
||||
const publish = await this.createEvent({
|
||||
kind: NDKKind.Contacts,
|
||||
tags: tags,
|
||||
});
|
||||
|
||||
if (publish) {
|
||||
this.account.contacts = tags.map((item) => item[1]);
|
||||
return publish;
|
||||
}
|
||||
}
|
||||
|
||||
public async createContact(pubkey: string) {
|
||||
const user = this.ndk.getUser({ pubkey: this.account.pubkey });
|
||||
const contacts = await user.follows();
|
||||
return await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
|
||||
}
|
||||
|
||||
public async deleteContact(pubkey: string) {
|
||||
const user = this.ndk.getUser({ pubkey: this.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 getCleanEventId(id: string) {
|
||||
let eventId: string = id.replace("nostr:", "").split("'")[0].split(".")[0];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return eventId;
|
||||
}
|
||||
|
||||
public async getEventById(id: string) {
|
||||
try {
|
||||
const eventId = this.getCleanEventId(id);
|
||||
return await this.ndk.fetchEvent(eventId);
|
||||
} catch {
|
||||
throw new Error("event not found");
|
||||
}
|
||||
}
|
||||
|
||||
public async getEventByFilter({
|
||||
filter,
|
||||
cache,
|
||||
}: { filter: NDKFilter; cache?: NDKSubscriptionCacheUsage }) {
|
||||
const event = await this.ndk.fetchEvent(filter, {
|
||||
cacheUsage: cache || NDKSubscriptionCacheUsage.CACHE_FIRST,
|
||||
});
|
||||
|
||||
if (!event) return null;
|
||||
return event;
|
||||
}
|
||||
|
||||
public async getEvents(filter: NDKFilter) {
|
||||
const events = await this.ndk.fetchEvents(filter);
|
||||
if (!events) return [];
|
||||
return [...events];
|
||||
}
|
||||
|
||||
public getEventThread({
|
||||
public parse_event_thread({
|
||||
content,
|
||||
tags,
|
||||
}: { content: string; tags: NDKTag[] }) {
|
||||
}: { content: string; tags: string[][] }) {
|
||||
let rootEventId: string = null;
|
||||
let replyEventId: string = null;
|
||||
|
||||
// Ignore quote repost
|
||||
if (content.includes("nostr:note1") || content.includes("nostr:nevent1"))
|
||||
return null;
|
||||
|
||||
// Get all event references from tags, ignore mention
|
||||
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
|
||||
|
||||
if (!events.length) return null;
|
||||
|
||||
if (events.length === 1)
|
||||
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];
|
||||
@@ -312,310 +67,24 @@ export class Ark {
|
||||
};
|
||||
}
|
||||
|
||||
public async getThreads(id: string) {
|
||||
const eventId = this.getCleanEventId(id);
|
||||
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
|
||||
const relayUrls = Array.from(this.ndk.pool.relays.keys());
|
||||
|
||||
public async get_metadata(id: string) {
|
||||
try {
|
||||
const rawEvents = (await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{
|
||||
kinds: [NDKKind.Text],
|
||||
"#e": [eventId],
|
||||
},
|
||||
{ since: 0 },
|
||||
{ sort: true },
|
||||
)) as unknown as NostrEvent[];
|
||||
|
||||
const 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 && el[3] !== "mention",
|
||||
);
|
||||
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;
|
||||
const cmd: Metadata = await invoke("get_metadata", { id });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
fetcher.shutdown();
|
||||
console.error("failed to get metadata", id);
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllRelaysFromContacts({ signal }: { signal: AbortSignal }) {
|
||||
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
|
||||
const connectedRelays = Array.from(this.ndk.pool.relays.keys());
|
||||
|
||||
public async user_to_bech32(key: string, relays: string[]) {
|
||||
try {
|
||||
const relayMap = new Map<string, string[]>();
|
||||
const relayEvents = fetcher.fetchLatestEventsPerAuthor(
|
||||
{
|
||||
authors: this.account.contacts,
|
||||
relayUrls: connectedRelays,
|
||||
},
|
||||
{ kinds: [NDKKind.RelayList] },
|
||||
1,
|
||||
{ abortSignal: signal },
|
||||
);
|
||||
|
||||
for await (const { author, events } of relayEvents) {
|
||||
if (events.length) {
|
||||
const relayTags = events[0].tags.filter((item) => item[0] === "r");
|
||||
for (const tag of relayTags) {
|
||||
const item = relayMap.get(tag[1]);
|
||||
if (item?.length) {
|
||||
item.push(author);
|
||||
} else {
|
||||
relayMap.set(tag[1], [author]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relayMap;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
fetcher.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public async getInfiniteEvents({
|
||||
filter,
|
||||
limit,
|
||||
pageParam = 0,
|
||||
signal = undefined,
|
||||
dedup = true,
|
||||
}: {
|
||||
filter: NDKFilter;
|
||||
limit: number;
|
||||
pageParam?: number;
|
||||
signal?: AbortSignal;
|
||||
dedup?: boolean;
|
||||
}) {
|
||||
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
|
||||
const relayUrls = Array.from(this.ndk.pool.relays.keys());
|
||||
const seenIds = new Set<string>();
|
||||
const dedupQueue = new Set<string>();
|
||||
|
||||
try {
|
||||
const events = await fetcher.fetchLatestEvents(relayUrls, filter, limit, {
|
||||
asOf: pageParam === 0 ? undefined : pageParam,
|
||||
abortSignal: signal,
|
||||
const cmd: string = await invoke("user_to_bech32", {
|
||||
key,
|
||||
relays,
|
||||
});
|
||||
|
||||
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")
|
||||
?.map((item) => item[1]);
|
||||
|
||||
if (tags.length) {
|
||||
for (const tag of tags) {
|
||||
if (seenIds.has(tag)) {
|
||||
dedupQueue.add(event.id);
|
||||
break;
|
||||
}
|
||||
|
||||
seenIds.add(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
fetcher.shutdown();
|
||||
return cmd;
|
||||
} catch {
|
||||
console.error("get nprofile id failed");
|
||||
}
|
||||
}
|
||||
|
||||
public async getRelayEvents({
|
||||
relayUrl,
|
||||
filter,
|
||||
limit,
|
||||
pageParam = 0,
|
||||
signal = undefined,
|
||||
}: {
|
||||
relayUrl: string;
|
||||
filter: NDKFilter;
|
||||
limit: number;
|
||||
pageParam?: number;
|
||||
signal?: AbortSignal;
|
||||
dedup?: boolean;
|
||||
}) {
|
||||
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
|
||||
|
||||
try {
|
||||
const events = await 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);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
fetcher.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 readFile(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 getAppRecommend({
|
||||
unknownKind,
|
||||
author,
|
||||
}: { unknownKind: string; author?: string }) {
|
||||
const event = await this.ndk.fetchEvent({
|
||||
kinds: [NDKKind.AppRecommendation],
|
||||
"#d": [unknownKind],
|
||||
authors: this.account.contacts || [author],
|
||||
});
|
||||
|
||||
if (event) return event.tags.filter((item) => item[0] !== "d");
|
||||
|
||||
const altEvent = await this.ndk.fetchEvent({
|
||||
kinds: [NDKKind.AppHandler],
|
||||
"#k": [unknownKind],
|
||||
authors: this.account.contacts || [author],
|
||||
});
|
||||
|
||||
if (altEvent) return altEvent.tags.filter((item) => item[0] !== "d");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getOAuthServices() {
|
||||
const trusted: NDKEvent[] = [];
|
||||
|
||||
const services = await this.ndk.fetchEvents({
|
||||
kinds: [NDKKind.AppHandler],
|
||||
"#k": ["24133"],
|
||||
});
|
||||
|
||||
for (const service of services) {
|
||||
const nip05 = JSON.parse(service.content).nip05 as string;
|
||||
try {
|
||||
const validate = await this.validateNIP05({
|
||||
pubkey: service.pubkey,
|
||||
nip05,
|
||||
});
|
||||
if (validate) trusted.push(service);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
return trusted;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user