Add simple landing page

This commit is contained in:
hzrd149 2024-09-25 15:28:28 -05:00
parent 50afa5b70d
commit bfc1b1c4c2
12 changed files with 265 additions and 179 deletions

View File

@ -0,0 +1,5 @@
---
"nsite-ts": minor
---
Add simple landing page

6
.env
View File

@ -1,6 +1,2 @@
CACHE_PATH="in-memory" CACHE_PATH="in-memory"
LOOKUP_RELAYS=wss://user.kindpag.es,wss://purplepag.es
NOSTR_RELAYS=wss://nos.lol,wss://relay.damus.io
BLOSSOM_SERVERS=https://cdn.hzrd149.com
NGINX_HOST='nginx'

View File

@ -15,7 +15,8 @@ services:
nsite: nsite:
build: . build: .
image: ghcr.io/hzrd149/nsite-ts:master
environment: environment:
NOSTR_RELAYS: wss://nos.lol,wss://relay.damus.io LOOKUP_RELAYS: wss://user.kindpag.es,wss://purplepag.es
ports: ports:
- 3000:80 - 3000:80

View File

@ -6,7 +6,7 @@ server {
proxy_cache request_cache; proxy_cache request_cache;
proxy_cache_valid 200 60m; proxy_cache_valid 200 60m;
proxy_cache_valid 404 10m; 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; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header X-Cache $upstream_cache_status; add_header X-Cache $upstream_cache_status;

View File

@ -14,33 +14,19 @@
</script> </script>
</head> </head>
<body> <body>
<label>relays</label> <h1>nsite-ts</h1>
<br /> <a href="https://github.com/hzrd149/nsite-ts" target="_blank">Source Code</a>
<textarea type="text" id="relays" cols="50" rows="4"></textarea>
<br /> <h2>Latest nsites:</h2>
<br /> <div id="sites"></div>
<label>blossom servers</label>
<br /> <template id="site">
<textarea type="text" id="servers" cols="50" rows="4"></textarea> <div class="site">
<br /> <a class="pubkey link"></a>
<br /> <span class="date"></span>
<input type="file" id="files" webkitdirectory directory multiple /> </div>
<button id="upload-button">Upload nsite</button> </template>
<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="./main.js"></script> <script type="module" src="./main.js"></script>
</body> </body>
</html> </html>

View File

@ -1,142 +1,32 @@
import { multiServerUpload, BlossomClient } from "blossom-client-sdk"; import { nip19, SimplePool } from "nostr-tools";
import { SimplePool } from "nostr-tools";
const logContainer = document.getElementById("log"); const seen = new Set();
function log(...args) { function addSite(event) {
const el = document.createElement("div"); if (seen.has(event.pubkey)) return;
el.innerText = args.join(" "); seen.add(event.pubkey);
logContainer.appendChild(el);
}
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} */ site.querySelector(".pubkey").textContent = npub;
const filesInput = document.getElementById("files"); site.querySelector(".link").href = new URL("/", `${location.protocol}//${npub}.${location.host}`).toString();
/** document.getElementById("sites").appendChild(site);
* @param {FileSystemFileEntry} fileEntry } catch (error) {
* @returns {File} console.log("Failed to add site", event);
*/ console.log(error);
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(); const pool = new SimplePool();
/** console.log("Loading sites");
* uploads a file system entry to blossom servers pool.subscribeMany(
* @param {{file:File, path:string}} files ["wss://relay.damus.io", "wss://nos.lol", "wss://nostr.wine"],
* @param {import("blossom-client-sdk").Signer} signer [{ kinds: [34128], "#d": ["/index.html"] }],
* @param {*} auth {
* @param {string[]} servers onevent: addSite,
* @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}`);
}
});

46
public/upload/index.html Normal file
View 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
View 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}`);
}
});

View File

@ -1,6 +1,7 @@
import "dotenv/config"; import "dotenv/config";
import xbytes from "xbytes"; 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 NOSTR_RELAYS = process.env.NOSTR_RELAYS?.split(",") ?? [];
const BLOSSOM_SERVERS = process.env.BLOSSOM_SERVERS?.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 NGINX_HOST = process.env.NGINX_HOST;
const CACHE_PATH = process.env.CACHE_PATH; 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, LOOKUP_RELAYS, BLOSSOM_SERVERS, MAX_FILE_SIZE, NGINX_HOST, CACHE_PATH };
export { NOSTR_RELAYS, BLOSSOM_SERVERS, MAX_FILE_SIZE, NGINX_HOST, CACHE_PATH };

View File

@ -1,6 +1,7 @@
import { extname, isAbsolute, join } from "path"; import { extname, isAbsolute, join } from "path";
import { NSITE_KIND } from "./const.js"; import { NSITE_KIND } from "./const.js";
import ndk from "./ndk.js"; import ndk from "./ndk.js";
import { NDKRelaySet } from "@nostr-dev-kit/ndk";
export function getSearchPaths(path: string) { export function getSearchPaths(path: string) {
const paths = [path]; const paths = [path];
@ -10,10 +11,10 @@ export function getSearchPaths(path: string) {
// also look for relative paths // also look for relative paths
for (const p of Array.from(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[][] }) { 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 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) return Array.from(events)
.map(parseNsiteEvent) .map(parseNsiteEvent)

View File

@ -13,7 +13,8 @@ import { resolveNpubFromHostname } from "./helpers/dns.js";
import { getNsiteBlobs } from "./events.js"; import { getNsiteBlobs } from "./events.js";
import { downloadFile, getUserBlossomServers } from "./blossom.js"; import { downloadFile, getUserBlossomServers } from "./blossom.js";
import { BLOSSOM_SERVERS } from "./env.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)); 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)); const pubkey = (ctx.state.pubkey = await resolveNpubFromHostname(ctx.hostname));
if (pubkey) { 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}`); 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) { if (blobs.length === 0) {
ctx.status = 404; ctx.status = 404;
@ -58,7 +67,7 @@ app.use(async (ctx, next) => {
return; return;
} }
let servers = await userServers.get<string[]>(pubkey); let servers = await userServers.get<string[] | undefined>(pubkey);
if (!servers) { if (!servers) {
console.log(`${pubkey}: Searching for blossom servers`); console.log(`${pubkey}: Searching for blossom servers`);
servers = (await getUserBlossomServers(pubkey)) ?? []; servers = (await getUserBlossomServers(pubkey)) ?? [];

View File

@ -1,10 +1,17 @@
import NDK from "@nostr-dev-kit/ndk"; 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({ const ndk = new NDK({
explicitRelayUrls: NOSTR_RELAYS, explicitRelayUrls: [...LOOKUP_RELAYS, ...NOSTR_RELAYS],
}); });
ndk.connect(); 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; export default ndk;