diff --git a/README.md b/README.md
index c7888b96..1bcece40 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,7 @@ Snort supports the following NIP's:
- [x] NIP-78: App specific data
- [ ] NIP-89: App handlers
- [x] NIP-94: File Metadata
+- [x] NIP-96: HTTP File Storage Integration (Draft)
- [x] NIP-98: HTTP Auth
### Running
diff --git a/packages/app/src/Const.ts b/packages/app/src/Const.ts
index 2984872a..904ccc34 100644
--- a/packages/app/src/Const.ts
+++ b/packages/app/src/Const.ts
@@ -13,11 +13,6 @@ export const Day = Hour * 24;
*/
export const ApiHost = "https://api.snort.social";
-/**
- * Void.cat file upload service url
- */
-export const VoidCatHost = "https://void.cat";
-
/**
* Kierans pubkey
*/
diff --git a/packages/app/src/Login/Preferences.ts b/packages/app/src/Login/Preferences.ts
index c4c6d50d..29497bb3 100644
--- a/packages/app/src/Login/Preferences.ts
+++ b/packages/app/src/Login/Preferences.ts
@@ -45,7 +45,7 @@ export interface UserPreferences {
/**
* File uploading service to upload attachments to
*/
- fileUploader: "void.cat" | "nostr.build" | "nostrimg.com";
+ fileUploader: "void.cat" | "nostr.build" | "nostrimg.com" | "void.cat-NIP96";
/**
* Use imgproxy to optimize images
diff --git a/packages/app/src/Pages/settings/Preferences.tsx b/packages/app/src/Pages/settings/Preferences.tsx
index d9a39aed..8c545357 100644
--- a/packages/app/src/Pages/settings/Preferences.tsx
+++ b/packages/app/src/Pages/settings/Preferences.tsx
@@ -471,6 +471,7 @@ const PreferencesPage = () => {
+
diff --git a/packages/app/src/Upload/Nip96.ts b/packages/app/src/Upload/Nip96.ts
new file mode 100644
index 00000000..bb815f23
--- /dev/null
+++ b/packages/app/src/Upload/Nip96.ts
@@ -0,0 +1,73 @@
+import { base64 } from "@scure/base";
+import { throwIfOffline } from "@snort/shared";
+import { EventPublisher, EventKind } from "@snort/system";
+import { UploadResult, Uploader } from "Upload";
+
+export class Nip96Uploader implements Uploader {
+ constructor(
+ readonly url: string,
+ readonly publisher: EventPublisher,
+ ) {}
+
+ get progress() {
+ return [];
+ }
+
+ async upload(file: File | Blob, filename: string): Promise {
+ throwIfOffline();
+ const auth = async (url: string, method: string) => {
+ const auth = await this.publisher.generic(eb => {
+ return eb.kind(EventKind.HttpAuthentication).tag(["u", url]).tag(["method", method]);
+ });
+ return `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(auth)))}`;
+ };
+
+ const fd = new FormData();
+ fd.append("size", file.size.toString());
+ fd.append("alt", filename);
+ fd.append("media_type", file.type);
+ fd.append("file", file);
+
+ const rsp = await fetch(this.url, {
+ body: fd,
+ method: "POST",
+ headers: {
+ accept: "application/json",
+ authorization: await auth(this.url, "POST"),
+ },
+ });
+ if (rsp.ok) {
+ throwIfOffline();
+ const data = (await rsp.json()) as Nip96Result;
+ if (data.status === "success") {
+ const dim = data.nip94_event.tags
+ .find(a => a[0] === "dim")
+ ?.at(1)
+ ?.split("x");
+ return {
+ url: data.nip94_event.tags.find(a => a[0] === "url")?.at(1),
+ metadata: {
+ width: dim?.at(0) ? Number(dim[0]) : undefined,
+ height: dim?.at(1) ? Number(dim[1]) : undefined,
+ },
+ };
+ }
+ return {
+ error: data.message,
+ };
+ }
+ return {
+ error: "Upload failed",
+ };
+ }
+}
+
+export interface Nip96Result {
+ status: string;
+ message: string;
+ processing_url?: string;
+ nip94_event: {
+ tags: Array>;
+ content: string;
+ };
+}
diff --git a/packages/app/src/Upload/VoidCat.ts b/packages/app/src/Upload/VoidCat.ts
index 14b99142..cef1db59 100644
--- a/packages/app/src/Upload/VoidCat.ts
+++ b/packages/app/src/Upload/VoidCat.ts
@@ -1,7 +1,7 @@
import { EventKind, EventPublisher } from "@snort/system";
import { UploadState, VoidApi } from "@void-cat/api";
-import { FileExtensionRegex, VoidCatHost } from "Const";
+import { FileExtensionRegex } from "Const";
import { UploadResult } from "Upload";
import { base64 } from "@scure/base";
import { throwIfOffline } from "@snort/shared";
@@ -26,7 +26,7 @@ export default async function VoidCatUpload(
return `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(auth)))}`;
}
: undefined;
- const api = new VoidApi(VoidCatHost, auth);
+ const api = new VoidApi("https://void.cat", auth);
const uploader = api.getUploader(
file,
sx => {
@@ -58,7 +58,7 @@ export default async function VoidCatUpload(
if (rsp.file?.metadata?.mimeType === "image/webp") {
ext = ["", "webp"];
}
- const resultUrl = rsp.file?.metadata?.url ?? `${VoidCatHost}/d/${rsp.file?.id}${ext ? `.${ext[1]}` : ""}`;
+ const resultUrl = rsp.file?.metadata?.url ?? `https://void.cat/d/${rsp.file?.id}${ext ? `.${ext[1]}` : ""}`;
const ret = {
url: resultUrl,
diff --git a/packages/app/src/Upload/index.ts b/packages/app/src/Upload/index.ts
index 28e5bc01..591274f6 100644
--- a/packages/app/src/Upload/index.ts
+++ b/packages/app/src/Upload/index.ts
@@ -7,8 +7,9 @@ import NostrBuild from "Upload/NostrBuild";
import VoidCat from "Upload/VoidCat";
import NostrImg from "Upload/NostrImg";
import { KieranPubKey } from "Const";
-import { bech32ToHex } from "SnortUtils";
+import { bech32ToHex, unwrap } from "SnortUtils";
import useEventPublisher from "Hooks/useEventPublisher";
+import { Nip96Uploader } from "./Nip96";
export interface UploadResult {
url?: string;
@@ -74,6 +75,9 @@ export default function useFileUpload(): Uploader {
progress: [],
} as Uploader;
}
+ case "void.cat-NIP96": {
+ return new Nip96Uploader("https://void.cat/nostr", unwrap(publisher));
+ }
case "nostrimg.com": {
return {
upload: NostrImg,