fix: default nip96
This commit is contained in:
@ -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<
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export class BlossomClient {}
|
@ -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) {
|
||||
|
Reference in New Issue
Block a user