fix: default nip96

This commit is contained in:
2024-09-12 14:56:06 +01:00
parent 736189d0d2
commit ce10d920f4
18 changed files with 139 additions and 417 deletions

View File

@ -14,8 +14,7 @@ import {
} from "@snort/system";
import { GiftsCache } from "@/Cache";
import SnortApi from "@/External/SnortApi";
import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils";
import { bech32ToHex, dedupeById, deleteRefCode, unwrap } from "@/Utils";
import { Blasters } from "@/Utils/Const";
import { LoginSession, LoginSessionType, LoginStore, SnortAppData } from "@/Utils/Login/index";
import { entropyToPrivateKey, generateBip39Entropy } from "@/Utils/nip6";
@ -41,31 +40,26 @@ export function clearEntropy(state: LoginSession) {
}
/**
* Generate a new key and login with this generated key
* Generate a new key
*/
export async function generateNewLoginKeys() {
const entropy = generateBip39Entropy();
const privateKey = await entropyToPrivateKey(entropy);
return { entropy, privateKey };
}
/**
* Login with newly generated key
*/
export async function generateNewLogin(
keys: { entropy: Uint8Array; privateKey: string },
system: SystemInterface,
pin: (key: string) => Promise<KeyStorage>,
profile: UserMetadata,
) {
const entropy = generateBip39Entropy();
const privateKey = await entropyToPrivateKey(entropy);
const { entropy, privateKey } = keys;
const newRelays = {} as Record<string, RelaySettings>;
// Use current timezone info to determine approx location
// use closest 5 relays
const country = getCountry();
const api = new SnortApi();
const closeRelays = await api.closeRelays(country.lat, country.lon, 20);
for (const cr of closeRelays.sort((a, b) => (a.distance > b.distance ? 1 : -1)).filter(a => !a.is_paid)) {
const rr = sanitizeRelayUrl(cr.url);
if (rr) {
newRelays[rr] = { read: true, write: true };
if (Object.keys(newRelays).length >= 5) {
break;
}
}
}
for (const [k, v] of Object.entries(CONFIG.defaultRelays)) {
if (!newRelays[k]) {
newRelays[k] = v;
@ -75,8 +69,8 @@ export async function generateNewLogin(
// connect to new relays
await Promise.all(Object.entries(newRelays).map(([k, v]) => system.ConnectToRelay(k, v)));
const publicKey = utils.bytesToHex(secp.schnorr.getPublicKey(privateKey));
const publisher = EventPublisher.privateKey(privateKey);
const publicKey = publisher.pubKey;
// Create new contact list following self and site account
const contactList = [publicKey, ...CONFIG.signUp.defaultFollows.map(a => bech32ToHex(a))].map(a => ["p", a]) as Array<

View File

@ -43,11 +43,6 @@ export interface UserPreferences {
*/
showDebugMenus: boolean;
/**
* File uploading service to upload attachments to
*/
fileUploader: "void.cat" | "nostr.build" | "nostrimg.com" | "void.cat-NIP96" | "nostrcheck.me" | "nip96";
/**
* Use imgproxy to optimize images
*/
@ -117,7 +112,6 @@ export const DefaultPreferences = {
confirmReposts: false,
showDebugMenus: true,
autoShowLatest: false,
fileUploader: "nostr.build",
imgProxyConfig: DefaultImgProxy,
defaultRootTab: "following",
defaultZapAmount: 50,

View File

@ -45,16 +45,6 @@ export class Nip96Uploader {
const data = (await rsp.json()) as Nip96Result;
if (data.status === "success") {
const meta = readNip94Tags(data.nip94_event.tags);
if (
meta.dimensions === undefined ||
meta.dimensions.length !== 2 ||
meta.dimensions[0] === 0 ||
meta.dimensions[1] === 0
) {
return {
error: `Invalid dimensions: "${meta.dimensions?.join("x")}"`,
};
}
return {
url: addExtensionToNip94Url(meta),
header: data.nip94_event,

View File

@ -1,71 +0,0 @@
import { base64 } from "@scure/base";
import { throwIfOffline } from "@snort/shared";
import { EventKind, EventPublisher } from "@snort/system";
import { UploadResult } from "@/Utils/Upload/index";
export default async function NostrBuild(file: File | Blob, publisher?: EventPublisher): Promise<UploadResult> {
const auth = publisher
? async (url: string, method: string) => {
const auth = await publisher.generic(eb => {
return eb.kind(EventKind.HttpAuthentication).tag(["u", url]).tag(["method", method]);
});
return `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(auth)))}`;
}
: undefined;
const fd = new FormData();
fd.append("fileToUpload", file);
fd.append("submit", "Upload Image");
const url = "https://nostr.build/api/v2/upload/files";
const headers = {
accept: "application/json",
} as Record<string, string>;
if (auth) {
headers["Authorization"] = await auth(url, "POST");
}
const rsp = await fetch(url, {
body: fd,
method: "POST",
headers,
});
if (rsp.ok) {
throwIfOffline();
const data = (await rsp.json()) as NostrBuildUploadResponse;
const res = data.data[0];
return {
url: res.url,
metadata: {
blurhash: res.blurhash,
width: res.dimensions.width,
height: res.dimensions.height,
hash: res.sha256,
},
};
}
return {
error: "Upload failed",
};
}
interface NostrBuildUploadResponse {
data: Array<NostrBuildUploadData>;
}
interface NostrBuildUploadData {
input_name: string;
name: string;
url: string;
thumbnail: string;
blurhash: string;
sha256: string;
type: string;
mime: string;
size: number;
metadata: Record<string, string>;
dimensions: {
width: number;
height: number;
};
}

View File

@ -1,44 +0,0 @@
import { throwIfOffline } from "@snort/shared";
import { UploadResult } from "@/Utils/Upload/index";
export default async function NostrImg(file: File | Blob): Promise<UploadResult> {
throwIfOffline();
const fd = new FormData();
fd.append("image", file);
const rsp = await fetch("https://nostrimg.com/api/upload", {
body: fd,
method: "POST",
headers: {
accept: "application/json",
},
});
if (rsp.ok) {
const data: UploadResponse = await rsp.json();
if (typeof data?.imageUrl === "string" && data.success) {
return {
url: new URL(data.imageUrl).toString(),
};
}
}
return {
error: "Upload failed",
};
}
interface UploadResponse {
fileID?: string;
fileName?: string;
imageUrl?: string;
lightningDestination?: string;
lightningPaymentLink?: string;
message?: string;
route?: string;
status: number;
success: boolean;
url?: string;
data?: {
url?: string;
};
}

View File

@ -1,102 +0,0 @@
import { base64 } from "@scure/base";
import { throwIfOffline } from "@snort/shared";
import { EventKind, EventPublisher } from "@snort/system";
import { UploadState, VoidApi } from "@void-cat/api";
import { FileExtensionRegex } from "@/Utils/Const";
import { UploadResult } from "@/Utils/Upload/index";
/**
* Upload file to void.cat
* https://void.cat/swagger/index.html
*/
export default async function VoidCatUpload(
file: File | Blob,
filename: string,
publisher?: EventPublisher,
progress?: (n: number) => void,
stage?: (n: "starting" | "hashing" | "uploading" | "done" | undefined) => void,
): Promise<UploadResult> {
throwIfOffline();
const auth = publisher
? async (url: string, method: string) => {
const auth = await publisher.generic(eb => {
return eb.kind(EventKind.HttpAuthentication).tag(["u", url]).tag(["method", method]);
});
return `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(auth)))}`;
}
: undefined;
const api = new VoidApi("https://void.cat", auth);
const uploader = api.getUploader(
file,
sx => {
stage?.(
(() => {
switch (sx) {
case UploadState.Starting:
return "starting";
case UploadState.Hashing:
return "hashing";
case UploadState.Uploading:
return "uploading";
case UploadState.Done:
return "done";
}
})(),
);
},
px => {
progress?.(px / file.size);
},
);
const rsp = await uploader.upload({
"V-Strip-Metadata": "true",
});
if (rsp.ok) {
let ext = filename.match(FileExtensionRegex);
if (rsp.file?.metadata?.mimeType === "image/webp") {
ext = ["", "webp"];
}
const resultUrl = rsp.file?.metadata?.url ?? `https://void.cat/d/${rsp.file?.id}${ext ? `.${ext[1]}` : ""}`;
const dim = rsp.file?.metadata?.mediaDimensions ? rsp.file.metadata.mediaDimensions.split("x") : undefined;
const ret = {
url: resultUrl,
metadata: {
hash: rsp.file?.metadata?.digest,
width: dim ? Number(dim[0]) : undefined,
height: dim ? Number(dim[1]) : undefined,
},
} as UploadResult;
if (publisher) {
// NIP-94
/*const tags = [
["url", resultUrl],
["x", rsp.file?.metadata?.digest ?? ""],
["m", rsp.file?.metadata?.mimeType ?? "application/octet-stream"],
];
if (rsp.file?.metadata?.size) {
tags.push(["size", rsp.file.metadata.size.toString()]);
}
if (rsp.file?.metadata?.magnetLink) {
tags.push(["magnet", rsp.file.metadata.magnetLink]);
const parsedMagnet = magnetURIDecode(rsp.file.metadata.magnetLink);
if (parsedMagnet?.infoHash) {
tags.push(["i", parsedMagnet?.infoHash]);
}
}
ret.header = await publisher.generic(eb => {
eb.kind(EventKind.FileHeader).content(filename);
tags.forEach(t => eb.tag(t));
return eb;
});*/
}
return ret;
} else {
return {
error: rsp.errorMessage,
};
}
}

View File

@ -1 +0,0 @@
export class BlossomClient {}

View File

@ -1,16 +1,9 @@
import { removeUndefined } from "@snort/shared";
import { EventKind, NostrEvent } from "@snort/system";
import { useState } from "react";
import { v4 as uuid } from "uuid";
import { EventPublisher, NostrEvent } from "@snort/system";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { useMediaServerList } from "@/Hooks/useMediaServerList";
import { bech32ToHex, randomSample, unwrap } from "@/Utils";
import { FileExtensionRegex, KieranPubKey } from "@/Utils/Const";
import NostrBuild from "@/Utils/Upload/NostrBuild";
import NostrImg from "@/Utils/Upload/NostrImg";
import VoidCat from "@/Utils/Upload/VoidCat";
import { Nip96Uploader } from "./Nip96";
@ -81,118 +74,19 @@ export interface UploadProgress {
export type UploadStage = "starting" | "hashing" | "uploading" | "done" | undefined;
export default function useFileUpload(): Uploader {
const fileUploader = usePreferences(s => s.fileUploader);
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
export default function useFileUpload(privKey?: string) {
const { publisher } = useEventPublisher();
const [progress, setProgress] = useState<Array<UploadProgress>>([]);
const [stage, setStage] = useState<UploadStage>();
const { servers } = useMediaServerList();
const defaultUploader = {
upload: async (f, n) => {
const id = uuid();
setProgress(s => [
...s,
{
id,
file: f,
progress: 0,
stage: undefined,
},
]);
const px = (n: number) => {
setProgress(s =>
s.map(v =>
v.id === id
? {
...v,
progress: n,
}
: v,
),
);
};
const ret = await VoidCat(f, n, publisher, px, s => setStage(s));
setProgress(s => s.filter(a => a.id !== id));
return ret;
},
progress,
stage,
} as Uploader;
switch (fileUploader) {
case "nostr.build": {
return {
upload: f => NostrBuild(f, publisher),
progress: [],
} as Uploader;
}
case "void.cat-NIP96": {
return new Nip96Uploader("https://void.cat/nostr", unwrap(publisher));
}
case "nostrcheck.me": {
return new Nip96Uploader("https://nostrcheck.me/api/v2/nip96", unwrap(publisher));
}
case "nostrimg.com": {
return {
upload: NostrImg,
progress: [],
} as Uploader;
}
case "nip96": {
const servers = removeUndefined(state.getList(EventKind.StorageServerList).map(a => a.toEventTag()?.at(1)));
if (servers.length > 0) {
const random = randomSample(servers, 1)[0];
return new Nip96Uploader(random, unwrap(publisher));
} else {
return defaultUploader;
}
}
default: {
return defaultUploader;
}
const pub = privKey ? EventPublisher.privateKey(privKey) : publisher;
if (servers.length > 0 && pub) {
const random = randomSample(servers, 1)[0];
return new Nip96Uploader(random, pub);
} else if (pub) {
return new Nip96Uploader("https://nostr.build", pub);
}
}
export const ProgressStream = (file: File | Blob, progress: (n: number) => void) => {
let offset = 0;
const DefaultChunkSize = 1024 * 32;
const readChunk = async (offset: number, size: number) => {
if (offset > file.size) {
return new Uint8Array(0);
}
const end = Math.min(offset + size, file.size);
const blob = file.slice(offset, end, file.type);
const data = await blob.arrayBuffer();
return new Uint8Array(data);
};
const rsBase = new ReadableStream(
{
start: async () => {},
pull: async controller => {
const chunk = await readChunk(offset, controller.desiredSize ?? DefaultChunkSize);
if (chunk.byteLength === 0) {
controller.close();
return;
}
progress((offset + chunk.byteLength) / file.size);
offset += chunk.byteLength;
controller.enqueue(chunk);
},
cancel: reason => {
console.log(reason);
},
type: "bytes",
},
{
highWaterMark: DefaultChunkSize,
},
);
return rsBase;
};
export function addExtensionToNip94Url(meta: Nip94Tags) {
if (!meta.url?.match(FileExtensionRegex) && meta.mimeType) {
switch (meta.mimeType) {