573 lines
11 KiB
TypeScript
573 lines
11 KiB
TypeScript
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
import type {
|
|
Account,
|
|
Contact,
|
|
Event,
|
|
EventWithReplies,
|
|
Keys,
|
|
Metadata,
|
|
} from "@lume/types";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { open } from "@tauri-apps/plugin-dialog";
|
|
import { readFile } from "@tauri-apps/plugin-fs";
|
|
import { generateContentTags } from "@lume/utils";
|
|
|
|
export class Ark {
|
|
public accounts: Account[];
|
|
|
|
constructor() {
|
|
this.accounts = [];
|
|
}
|
|
|
|
public async get_all_accounts() {
|
|
try {
|
|
const accounts: Account[] = [];
|
|
const cmd: string[] = await invoke("get_accounts");
|
|
|
|
if (cmd) {
|
|
for (const item of cmd) {
|
|
accounts.push({ npub: item.replace(".npub", "") });
|
|
}
|
|
|
|
this.accounts = accounts;
|
|
return accounts;
|
|
}
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public async load_selected_account(npub: string) {
|
|
try {
|
|
const cmd: boolean = await invoke("load_selected_account", {
|
|
npub,
|
|
});
|
|
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async create_guest_account() {
|
|
try {
|
|
const keys = await this.create_keys();
|
|
await this.save_account(keys.nsec, "");
|
|
|
|
return keys.npub;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async create_keys() {
|
|
try {
|
|
const cmd: Keys = await invoke("create_keys");
|
|
return cmd;
|
|
} catch (e) {
|
|
console.error(String(e));
|
|
}
|
|
}
|
|
|
|
public async save_account(nsec: string, password: string = "") {
|
|
try {
|
|
const cmd: boolean = await invoke("save_key", {
|
|
nsec,
|
|
password,
|
|
});
|
|
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async event_to_bech32(id: string, relays: string[]) {
|
|
try {
|
|
const cmd: string = await invoke("event_to_bech32", {
|
|
id,
|
|
relays,
|
|
});
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async get_event(id: string) {
|
|
try {
|
|
const eventId: string = id
|
|
.replace("nostr:", "")
|
|
.split("'")[0]
|
|
.split(".")[0];
|
|
const cmd: string = await invoke("get_event", { id: eventId });
|
|
const event: Event = JSON.parse(cmd);
|
|
return event;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async get_events_from(id: string, limit: number, asOf?: number) {
|
|
try {
|
|
let until: string = undefined;
|
|
if (asOf && asOf > 0) until = asOf.toString();
|
|
|
|
const nostrEvents: Event[] = await invoke("get_events_from", {
|
|
id,
|
|
limit,
|
|
until,
|
|
});
|
|
|
|
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public async get_events(
|
|
type: "local" | "global",
|
|
limit: number,
|
|
asOf?: number,
|
|
dedup?: boolean,
|
|
) {
|
|
try {
|
|
let until: string = undefined;
|
|
if (asOf && asOf > 0) until = asOf.toString();
|
|
|
|
const seenIds = new Set<string>();
|
|
const dedupQueue = new Set<string>();
|
|
|
|
const nostrEvents: Event[] = await invoke(`get_${type}_events`, {
|
|
limit,
|
|
until,
|
|
});
|
|
|
|
if (dedup) {
|
|
for (const event of nostrEvents) {
|
|
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 nostrEvents
|
|
.filter((event) => !dedupQueue.has(event.id))
|
|
.sort((a, b) => b.created_at - a.created_at);
|
|
}
|
|
|
|
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public async publish(content: string, reply_to?: string, quote?: boolean) {
|
|
try {
|
|
const g = await generateContentTags(content);
|
|
|
|
const eventContent = g.content;
|
|
const eventTags = g.tags;
|
|
|
|
if (reply_to) {
|
|
const replyEvent = await this.get_event(reply_to);
|
|
|
|
if (quote) {
|
|
eventTags.push([
|
|
"e",
|
|
replyEvent.id,
|
|
replyEvent.relay || "",
|
|
"mention",
|
|
]);
|
|
} else {
|
|
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
|
|
|
|
if (rootEvent) {
|
|
eventTags.push(["e", rootEvent[1], rootEvent[2] || "", "root"]);
|
|
}
|
|
|
|
eventTags.push(["e", replyEvent.id, replyEvent.relay || "", "reply"]);
|
|
eventTags.push(["p", replyEvent.pubkey]);
|
|
}
|
|
}
|
|
|
|
const cmd: string = await invoke("publish", {
|
|
content: eventContent,
|
|
tags: eventTags,
|
|
});
|
|
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async reply_to(content: string, tags: string[]) {
|
|
try {
|
|
const cmd: string = await invoke("reply_to", { content, tags });
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async repost(id: string, author: string) {
|
|
try {
|
|
const cmd: string = await invoke("repost", { id, pubkey: author });
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async upvote(id: string, author: string) {
|
|
try {
|
|
const cmd: string = await invoke("upvote", { id, pubkey: author });
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async downvote(id: string, author: string) {
|
|
try {
|
|
const cmd: string = await invoke("downvote", { id, pubkey: author });
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async get_event_thread(id: string) {
|
|
try {
|
|
const events: EventWithReplies[] = await invoke("get_event_thread", {
|
|
id,
|
|
});
|
|
|
|
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;
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public parse_event_thread({
|
|
content,
|
|
tags,
|
|
}: { 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) {
|
|
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 get_profile(pubkey: string) {
|
|
try {
|
|
const id = pubkey
|
|
.replace("nostr:", "")
|
|
.split("'")[0]
|
|
.split(".")[0]
|
|
.split(",")[0]
|
|
.split("?")[0];
|
|
const cmd: Metadata = await invoke("get_profile", { id });
|
|
|
|
return cmd;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async get_contact_list() {
|
|
try {
|
|
const cmd: string[] = await invoke("get_contact_list");
|
|
return cmd;
|
|
} catch (e) {
|
|
console.error(e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public async get_contact_metadata() {
|
|
try {
|
|
const cmd: Contact[] = await invoke("get_contact_metadata");
|
|
return cmd;
|
|
} catch (e) {
|
|
console.error(e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public async follow(id: string, alias?: string) {
|
|
try {
|
|
const cmd: string = await invoke("follow", { id, alias });
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async unfollow(id: string) {
|
|
try {
|
|
const cmd: string = await invoke("unfollow", { id });
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async user_to_bech32(key: string, relays: string[]) {
|
|
try {
|
|
const cmd: string = await invoke("user_to_bech32", {
|
|
key,
|
|
relays,
|
|
});
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async verify_nip05(pubkey: string, nip05: string) {
|
|
try {
|
|
const cmd: boolean = await invoke("verify_nip05", {
|
|
key: pubkey,
|
|
nip05,
|
|
});
|
|
return cmd;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async set_nwc(uri: string) {
|
|
try {
|
|
const cmd: boolean = await invoke("set_nwc", { uri });
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async load_nwc() {
|
|
try {
|
|
const cmd: boolean = await invoke("load_nwc");
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async get_balance() {
|
|
try {
|
|
const cmd: number = await invoke("get_balance");
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async zap_profile(id: string, amount: number, message: string) {
|
|
try {
|
|
const cmd: boolean = await invoke("zap_profile", { id, amount, message });
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async zap_event(id: string, amount: number, message: string) {
|
|
try {
|
|
const cmd: boolean = await invoke("zap_event", { id, amount, message });
|
|
return cmd;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public async upload(filePath?: string) {
|
|
try {
|
|
const allowExts = [
|
|
"png",
|
|
"jpeg",
|
|
"jpg",
|
|
"gif",
|
|
"mp4",
|
|
"mp3",
|
|
"webm",
|
|
"mkv",
|
|
"avi",
|
|
"mov",
|
|
];
|
|
|
|
const selected =
|
|
filePath ||
|
|
(
|
|
await open({
|
|
multiple: false,
|
|
filters: [
|
|
{
|
|
name: "Media",
|
|
extensions: allowExts,
|
|
},
|
|
],
|
|
})
|
|
).path;
|
|
|
|
if (!selected) return null;
|
|
|
|
const file = await readFile(selected);
|
|
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;
|
|
} catch (e) {
|
|
throw new Error(String(e));
|
|
}
|
|
}
|
|
|
|
public open_thread(id: string) {
|
|
return new WebviewWindow(`event-${id}`, {
|
|
title: "Thread",
|
|
url: `/events/${id}`,
|
|
minWidth: 500,
|
|
width: 600,
|
|
height: 800,
|
|
hiddenTitle: true,
|
|
titleBarStyle: "overlay",
|
|
});
|
|
}
|
|
|
|
public open_profile(pubkey: string) {
|
|
return new WebviewWindow(`user-${pubkey}`, {
|
|
title: "Profile",
|
|
url: `/users/${pubkey}`,
|
|
minWidth: 500,
|
|
width: 500,
|
|
height: 800,
|
|
hiddenTitle: true,
|
|
titleBarStyle: "overlay",
|
|
});
|
|
}
|
|
|
|
public open_editor(reply_to?: string, quote: boolean = false) {
|
|
let url: string;
|
|
|
|
if (reply_to) {
|
|
url = `/editor?reply_to=${reply_to}"e=${quote}`;
|
|
} else {
|
|
url = "/editor";
|
|
}
|
|
|
|
return new WebviewWindow("editor", {
|
|
title: "Editor",
|
|
url,
|
|
minWidth: 500,
|
|
width: 600,
|
|
height: 400,
|
|
hiddenTitle: true,
|
|
titleBarStyle: "overlay",
|
|
fileDropEnabled: true,
|
|
});
|
|
}
|
|
|
|
public open_nwc() {
|
|
return new WebviewWindow("nwc", {
|
|
title: "Nostr Wallet Connect",
|
|
url: "/nwc",
|
|
minWidth: 400,
|
|
width: 400,
|
|
height: 600,
|
|
hiddenTitle: true,
|
|
titleBarStyle: "overlay",
|
|
fileDropEnabled: true,
|
|
});
|
|
}
|
|
|
|
public open_zap(id: string, pubkey: string, account: string) {
|
|
return new WebviewWindow(`zap-${id}`, {
|
|
title: "Nostr Wallet Connect",
|
|
url: `/zap/${id}?pubkey=${pubkey}&account=${account}`,
|
|
minWidth: 400,
|
|
width: 400,
|
|
height: 500,
|
|
hiddenTitle: true,
|
|
titleBarStyle: "overlay",
|
|
fileDropEnabled: true,
|
|
});
|
|
}
|
|
}
|