mirror of
https://github.com/hzrd149/nsite-gateway.git
synced 2025-06-23 20:05:03 +00:00
Add simple landing page
This commit is contained in:
parent
50afa5b70d
commit
bfc1b1c4c2
5
.changeset/hot-llamas-clap.md
Normal file
5
.changeset/hot-llamas-clap.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nsite-ts": minor
|
||||
---
|
||||
|
||||
Add simple landing page
|
6
.env
6
.env
@ -1,6 +1,2 @@
|
||||
CACHE_PATH="in-memory"
|
||||
|
||||
NOSTR_RELAYS=wss://nos.lol,wss://relay.damus.io
|
||||
BLOSSOM_SERVERS=https://cdn.hzrd149.com
|
||||
|
||||
NGINX_HOST='nginx'
|
||||
LOOKUP_RELAYS=wss://user.kindpag.es,wss://purplepag.es
|
||||
|
@ -15,7 +15,8 @@ services:
|
||||
|
||||
nsite:
|
||||
build: .
|
||||
image: ghcr.io/hzrd149/nsite-ts:master
|
||||
environment:
|
||||
NOSTR_RELAYS: wss://nos.lol,wss://relay.damus.io
|
||||
LOOKUP_RELAYS: wss://user.kindpag.es,wss://purplepag.es
|
||||
ports:
|
||||
- 3000:80
|
||||
|
@ -6,7 +6,7 @@ server {
|
||||
proxy_cache request_cache;
|
||||
proxy_cache_valid 200 60m;
|
||||
proxy_cache_valid 404 10m;
|
||||
proxy_cache_key $scheme$proxy_host$request_uri;
|
||||
proxy_cache_key $scheme$host$request_uri;
|
||||
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
|
@ -14,33 +14,19 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<label>relays</label>
|
||||
<br />
|
||||
<textarea type="text" id="relays" cols="50" rows="4"></textarea>
|
||||
<br />
|
||||
<br />
|
||||
<label>blossom servers</label>
|
||||
<br />
|
||||
<textarea type="text" id="servers" cols="50" rows="4"></textarea>
|
||||
<br />
|
||||
<br />
|
||||
<input type="file" id="files" webkitdirectory directory multiple />
|
||||
<button id="upload-button">Upload nsite</button>
|
||||
<div
|
||||
id="log"
|
||||
style="
|
||||
max-height: 50em;
|
||||
max-width: 80em;
|
||||
width: 100%;
|
||||
border: 1px solid gray;
|
||||
min-height: 8em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
font-size: 0.8em;
|
||||
gap: 0.1em;
|
||||
white-space: pre;
|
||||
"
|
||||
></div>
|
||||
<h1>nsite-ts</h1>
|
||||
<a href="https://github.com/hzrd149/nsite-ts" target="_blank">Source Code</a>
|
||||
|
||||
<h2>Latest nsites:</h2>
|
||||
<div id="sites"></div>
|
||||
|
||||
<template id="site">
|
||||
<div class="site">
|
||||
<a class="pubkey link"></a>
|
||||
<span class="date"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="module" src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
156
public/main.js
156
public/main.js
@ -1,142 +1,32 @@
|
||||
import { multiServerUpload, BlossomClient } from "blossom-client-sdk";
|
||||
import { SimplePool } from "nostr-tools";
|
||||
import { nip19, SimplePool } from "nostr-tools";
|
||||
|
||||
const logContainer = document.getElementById("log");
|
||||
function log(...args) {
|
||||
const el = document.createElement("div");
|
||||
el.innerText = args.join(" ");
|
||||
logContainer.appendChild(el);
|
||||
}
|
||||
const seen = new Set();
|
||||
function addSite(event) {
|
||||
if (seen.has(event.pubkey)) return;
|
||||
seen.add(event.pubkey);
|
||||
|
||||
const uploadButton = document.getElementById("upload-button");
|
||||
try {
|
||||
const template = document.getElementById("site");
|
||||
const site = template.content.cloneNode(true);
|
||||
const npub = nip19.npubEncode(event.pubkey);
|
||||
|
||||
/** @type {HTMLInputElement} */
|
||||
const filesInput = document.getElementById("files");
|
||||
site.querySelector(".pubkey").textContent = npub;
|
||||
site.querySelector(".link").href = new URL("/", `${location.protocol}//${npub}.${location.host}`).toString();
|
||||
|
||||
/**
|
||||
* @param {FileSystemFileEntry} fileEntry
|
||||
* @returns {File}
|
||||
*/
|
||||
export function readFileSystemFile(fileEntry) {
|
||||
return new Promise((res, rej) => {
|
||||
fileEntry.file(
|
||||
(file) => res(file),
|
||||
(err) => rej(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FileSystemDirectoryEntry} directory
|
||||
* @returns {FileSystemEntry[]}
|
||||
*/
|
||||
export function readFileSystemDirectory(directory) {
|
||||
return new Promise((res, rej) => {
|
||||
directory.createReader().readEntries(
|
||||
(entries) => res(entries),
|
||||
(err) => rej(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* uploads a file system entry to blossom servers
|
||||
* @param {FileSystemEntry} entry
|
||||
* @returns {{file: File, path: string, sha256: string}[]}
|
||||
*/
|
||||
async function readFileSystemEntry(entry) {
|
||||
const files = [];
|
||||
if (entry instanceof FileSystemFileEntry && entry.isFile) {
|
||||
try {
|
||||
const file = await readFileSystemFile(entry);
|
||||
const sha256 = await BlossomClient.getFileSha256(file);
|
||||
const path = entry.fullPath;
|
||||
|
||||
files.push({ file, path, sha256 });
|
||||
} catch (e) {
|
||||
log("Failed to add" + entry.fullPath);
|
||||
log(e.message);
|
||||
}
|
||||
} else if (entry instanceof FileSystemDirectoryEntry && entry.isDirectory) {
|
||||
const entries = await readFileSystemDirectory(entry);
|
||||
for (const e of entries) files.push(...(await readFileSystemEntry(e)));
|
||||
document.getElementById("sites").appendChild(site);
|
||||
} catch (error) {
|
||||
console.log("Failed to add site", event);
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* uploads a file system entry to blossom servers
|
||||
* @param {FileList} list
|
||||
* @returns {{file: File, path: string, sha256: string}[]}
|
||||
*/
|
||||
async function readFileList(list) {
|
||||
const files = [];
|
||||
for (const file of list) {
|
||||
const path = file.webkitRelativePath ? file.webkitRelativePath : file.name;
|
||||
const sha256 = await BlossomClient.getFileSha256(file);
|
||||
files.push({ file, path, sha256 });
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const pool = new SimplePool();
|
||||
|
||||
/**
|
||||
* uploads a file system entry to blossom servers
|
||||
* @param {{file:File, path:string}} files
|
||||
* @param {import("blossom-client-sdk").Signer} signer
|
||||
* @param {*} auth
|
||||
* @param {string[]} servers
|
||||
* @param {string[]} relays
|
||||
*/
|
||||
async function uploadFiles(files, signer, auth, servers, relays) {
|
||||
for (const { file, path, sha256 } of files) {
|
||||
try {
|
||||
const upload = multiServerUpload(servers, file, signer, auth);
|
||||
|
||||
let published = false;
|
||||
for await (let { blob } of upload) {
|
||||
if (!published) {
|
||||
const signed = await signer({
|
||||
kind: 34128,
|
||||
content: "",
|
||||
created_at: Math.round(Date.now() / 1000),
|
||||
tags: [
|
||||
["d", path],
|
||||
["x", sha256],
|
||||
],
|
||||
});
|
||||
await pool.publish(relays, signed);
|
||||
|
||||
log("Published", path, sha256, signed.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log(`Failed to upload ${path}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadButton.addEventListener("click", async () => {
|
||||
if (!window.nostr) return alert("Missing NIP-07 signer");
|
||||
|
||||
const signer = (draft) => window.nostr.signEvent(draft);
|
||||
const relays = document.getElementById("relays").value.split(/\n|,/);
|
||||
const servers = document.getElementById("servers").value.split(/\n|,/);
|
||||
|
||||
try {
|
||||
if (filesInput.files) {
|
||||
const files = await readFileList(filesInput.files);
|
||||
|
||||
// strip leading dir
|
||||
for (const file of files) file.path = file.path.replace(/^[^\/]+\//, "/");
|
||||
|
||||
log(`Found ${files.length} files`);
|
||||
|
||||
await uploadFiles(files, signer, undefined, servers, relays);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Failed to upload files: ${error.message}`);
|
||||
}
|
||||
});
|
||||
console.log("Loading sites");
|
||||
pool.subscribeMany(
|
||||
["wss://relay.damus.io", "wss://nos.lol", "wss://nostr.wine"],
|
||||
[{ kinds: [34128], "#d": ["/index.html"] }],
|
||||
{
|
||||
onevent: addSite,
|
||||
},
|
||||
);
|
||||
|
46
public/upload/index.html
Normal file
46
public/upload/index.html
Normal file
@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>nsite</title>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"blossom-client-sdk": "https://esm.run/blossom-client-sdk",
|
||||
"nostr-tools": "https://esm.run/nostr-tools"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<label>relays</label>
|
||||
<br />
|
||||
<textarea type="text" id="relays" cols="50" rows="4"></textarea>
|
||||
<br />
|
||||
<br />
|
||||
<label>blossom servers</label>
|
||||
<br />
|
||||
<textarea type="text" id="servers" cols="50" rows="4"></textarea>
|
||||
<br />
|
||||
<br />
|
||||
<input type="file" id="files" webkitdirectory directory multiple />
|
||||
<button id="upload-button">Upload nsite</button>
|
||||
<div
|
||||
id="log"
|
||||
style="
|
||||
max-height: 50em;
|
||||
max-width: 80em;
|
||||
width: 100%;
|
||||
border: 1px solid gray;
|
||||
min-height: 8em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
font-size: 0.8em;
|
||||
gap: 0.1em;
|
||||
white-space: pre;
|
||||
"
|
||||
></div>
|
||||
<script type="module" src="./upload.js"></script>
|
||||
</body>
|
||||
</html>
|
142
public/upload/upload.js
Normal file
142
public/upload/upload.js
Normal file
@ -0,0 +1,142 @@
|
||||
import { multiServerUpload, BlossomClient } from "blossom-client-sdk";
|
||||
import { SimplePool } from "nostr-tools";
|
||||
|
||||
const logContainer = document.getElementById("log");
|
||||
function log(...args) {
|
||||
const el = document.createElement("div");
|
||||
el.innerText = args.join(" ");
|
||||
logContainer.appendChild(el);
|
||||
}
|
||||
|
||||
const uploadButton = document.getElementById("upload-button");
|
||||
|
||||
/** @type {HTMLInputElement} */
|
||||
const filesInput = document.getElementById("files");
|
||||
|
||||
/**
|
||||
* @param {FileSystemFileEntry} fileEntry
|
||||
* @returns {File}
|
||||
*/
|
||||
export function readFileSystemFile(fileEntry) {
|
||||
return new Promise((res, rej) => {
|
||||
fileEntry.file(
|
||||
(file) => res(file),
|
||||
(err) => rej(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FileSystemDirectoryEntry} directory
|
||||
* @returns {FileSystemEntry[]}
|
||||
*/
|
||||
export function readFileSystemDirectory(directory) {
|
||||
return new Promise((res, rej) => {
|
||||
directory.createReader().readEntries(
|
||||
(entries) => res(entries),
|
||||
(err) => rej(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* uploads a file system entry to blossom servers
|
||||
* @param {FileSystemEntry} entry
|
||||
* @returns {{file: File, path: string, sha256: string}[]}
|
||||
*/
|
||||
async function readFileSystemEntry(entry) {
|
||||
const files = [];
|
||||
if (entry instanceof FileSystemFileEntry && entry.isFile) {
|
||||
try {
|
||||
const file = await readFileSystemFile(entry);
|
||||
const sha256 = await BlossomClient.getFileSha256(file);
|
||||
const path = entry.fullPath;
|
||||
|
||||
files.push({ file, path, sha256 });
|
||||
} catch (e) {
|
||||
log("Failed to add" + entry.fullPath);
|
||||
log(e.message);
|
||||
}
|
||||
} else if (entry instanceof FileSystemDirectoryEntry && entry.isDirectory) {
|
||||
const entries = await readFileSystemDirectory(entry);
|
||||
for (const e of entries) files.push(...(await readFileSystemEntry(e)));
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* uploads a file system entry to blossom servers
|
||||
* @param {FileList} list
|
||||
* @returns {{file: File, path: string, sha256: string}[]}
|
||||
*/
|
||||
async function readFileList(list) {
|
||||
const files = [];
|
||||
for (const file of list) {
|
||||
const path = file.webkitRelativePath ? file.webkitRelativePath : file.name;
|
||||
const sha256 = await BlossomClient.getFileSha256(file);
|
||||
files.push({ file, path, sha256 });
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const pool = new SimplePool();
|
||||
|
||||
/**
|
||||
* uploads a file system entry to blossom servers
|
||||
* @param {{file:File, path:string}} files
|
||||
* @param {import("blossom-client-sdk").Signer} signer
|
||||
* @param {*} auth
|
||||
* @param {string[]} servers
|
||||
* @param {string[]} relays
|
||||
*/
|
||||
async function uploadFiles(files, signer, auth, servers, relays) {
|
||||
for (const { file, path, sha256 } of files) {
|
||||
try {
|
||||
const upload = multiServerUpload(servers, file, signer, auth);
|
||||
|
||||
let published = false;
|
||||
for await (let { blob } of upload) {
|
||||
if (!published) {
|
||||
const signed = await signer({
|
||||
kind: 34128,
|
||||
content: "",
|
||||
created_at: Math.round(Date.now() / 1000),
|
||||
tags: [
|
||||
["d", path],
|
||||
["x", sha256],
|
||||
],
|
||||
});
|
||||
await pool.publish(relays, signed);
|
||||
|
||||
log("Published", path, sha256, signed.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log(`Failed to upload ${path}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadButton.addEventListener("click", async () => {
|
||||
if (!window.nostr) return alert("Missing NIP-07 signer");
|
||||
|
||||
const signer = (draft) => window.nostr.signEvent(draft);
|
||||
const relays = document.getElementById("relays").value.split(/\n|,/);
|
||||
const servers = document.getElementById("servers").value.split(/\n|,/);
|
||||
|
||||
try {
|
||||
if (filesInput.files) {
|
||||
const files = await readFileList(filesInput.files);
|
||||
|
||||
// strip leading dir
|
||||
for (const file of files) file.path = file.path.replace(/^[^\/]+\//, "/");
|
||||
|
||||
log(`Found ${files.length} files`);
|
||||
|
||||
await uploadFiles(files, signer, undefined, servers, relays);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Failed to upload files: ${error.message}`);
|
||||
}
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
import "dotenv/config";
|
||||
import xbytes from "xbytes";
|
||||
|
||||
const LOOKUP_RELAYS = process.env.LOOKUP_RELAYS?.split(",") ?? ["wss://user.kindpag.es/", "wss://purplepag.es/"];
|
||||
const NOSTR_RELAYS = process.env.NOSTR_RELAYS?.split(",") ?? [];
|
||||
const BLOSSOM_SERVERS = process.env.BLOSSOM_SERVERS?.split(",") ?? [];
|
||||
|
||||
@ -9,6 +10,4 @@ const MAX_FILE_SIZE = process.env.MAX_FILE_SIZE ? xbytes.parseSize(process.env.M
|
||||
const NGINX_HOST = process.env.NGINX_HOST;
|
||||
const CACHE_PATH = process.env.CACHE_PATH;
|
||||
|
||||
if (NOSTR_RELAYS.length === 0) throw new Error("Requires at least one relay in NOSTR_RELAYS");
|
||||
|
||||
export { NOSTR_RELAYS, BLOSSOM_SERVERS, MAX_FILE_SIZE, NGINX_HOST, CACHE_PATH };
|
||||
export { NOSTR_RELAYS, LOOKUP_RELAYS, BLOSSOM_SERVERS, MAX_FILE_SIZE, NGINX_HOST, CACHE_PATH };
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { extname, isAbsolute, join } from "path";
|
||||
import { NSITE_KIND } from "./const.js";
|
||||
import ndk from "./ndk.js";
|
||||
import { NDKRelaySet } from "@nostr-dev-kit/ndk";
|
||||
|
||||
export function getSearchPaths(path: string) {
|
||||
const paths = [path];
|
||||
@ -10,10 +11,10 @@ export function getSearchPaths(path: string) {
|
||||
|
||||
// also look for relative paths
|
||||
for (const p of Array.from(paths)) {
|
||||
if (isAbsolute(p)) paths.push(path.replace(/^\//, ""));
|
||||
if (isAbsolute(p)) paths.push(p.replace(/^\//, ""));
|
||||
}
|
||||
|
||||
return paths;
|
||||
return paths.filter((p) => !!p);
|
||||
}
|
||||
|
||||
export function parseNsiteEvent(event: { pubkey: string; tags: string[][] }) {
|
||||
@ -28,9 +29,13 @@ export function parseNsiteEvent(event: { pubkey: string; tags: string[][] }) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getNsiteBlobs(pubkey: string, path: string) {
|
||||
export async function getNsiteBlobs(pubkey: string, path: string, relays?: string[]) {
|
||||
const paths = getSearchPaths(path);
|
||||
const events = await ndk.fetchEvents({ kinds: [NSITE_KIND], "#d": paths, authors: [pubkey] });
|
||||
const events = await ndk.fetchEvents(
|
||||
{ kinds: [NSITE_KIND], "#d": paths, authors: [pubkey] },
|
||||
{},
|
||||
relays && NDKRelaySet.fromRelayUrls(relays, ndk, true),
|
||||
);
|
||||
|
||||
return Array.from(events)
|
||||
.map(parseNsiteEvent)
|
||||
|
15
src/index.ts
15
src/index.ts
@ -13,7 +13,8 @@ import { resolveNpubFromHostname } from "./helpers/dns.js";
|
||||
import { getNsiteBlobs } from "./events.js";
|
||||
import { downloadFile, getUserBlossomServers } from "./blossom.js";
|
||||
import { BLOSSOM_SERVERS } from "./env.js";
|
||||
import { userServers } from "./cache.js";
|
||||
import { userRelays, userServers } from "./cache.js";
|
||||
import { getUserOutboxes } from "./ndk.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@ -49,8 +50,16 @@ app.use(async (ctx, next) => {
|
||||
const pubkey = (ctx.state.pubkey = await resolveNpubFromHostname(ctx.hostname));
|
||||
|
||||
if (pubkey) {
|
||||
console.log(`${pubkey}: Fetching relays`);
|
||||
|
||||
let relays = await userRelays.get<string[] | undefined>(pubkey);
|
||||
if (!relays) {
|
||||
relays = await getUserOutboxes(pubkey);
|
||||
if (relays) await userRelays.set(pubkey, relays);
|
||||
}
|
||||
|
||||
console.log(`${pubkey}: Searching for ${ctx.path}`);
|
||||
const blobs = await getNsiteBlobs(pubkey, ctx.path);
|
||||
const blobs = await getNsiteBlobs(pubkey, ctx.path, relays);
|
||||
|
||||
if (blobs.length === 0) {
|
||||
ctx.status = 404;
|
||||
@ -58,7 +67,7 @@ app.use(async (ctx, next) => {
|
||||
return;
|
||||
}
|
||||
|
||||
let servers = await userServers.get<string[]>(pubkey);
|
||||
let servers = await userServers.get<string[] | undefined>(pubkey);
|
||||
if (!servers) {
|
||||
console.log(`${pubkey}: Searching for blossom servers`);
|
||||
servers = (await getUserBlossomServers(pubkey)) ?? [];
|
||||
|
11
src/ndk.ts
11
src/ndk.ts
@ -1,10 +1,17 @@
|
||||
import NDK from "@nostr-dev-kit/ndk";
|
||||
import { NOSTR_RELAYS } from "./env.js";
|
||||
import { LOOKUP_RELAYS, NOSTR_RELAYS } from "./env.js";
|
||||
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: NOSTR_RELAYS,
|
||||
explicitRelayUrls: [...LOOKUP_RELAYS, ...NOSTR_RELAYS],
|
||||
});
|
||||
|
||||
ndk.connect();
|
||||
|
||||
export async function getUserOutboxes(pubkey: string) {
|
||||
const mailboxes = await ndk.fetchEvent({ kinds: [10002], authors: [pubkey] });
|
||||
if (!mailboxes) return;
|
||||
|
||||
return mailboxes.tags.filter((t) => t[0] === "r" && (t[2] === undefined || t[2] === "write")).map((t) => t[1]);
|
||||
}
|
||||
|
||||
export default ndk;
|
||||
|
Loading…
x
Reference in New Issue
Block a user