Add screenshots for nsites

This commit is contained in:
hzrd149 2024-10-04 10:32:57 -05:00
parent 1d3c9e1f6a
commit f25e2409e9
17 changed files with 809 additions and 8162 deletions

View File

@ -0,0 +1,5 @@
---
"nsite-ts": minor
---
Add screenshots for nsites

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ build
.env .env
data data
.netrc .netrc
screenshots

3
.vscode/launch.json vendored
View File

@ -26,8 +26,7 @@
"internalConsoleOptions": "openOnSessionStart", "internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std", "outputCapture": "std",
"env": { "env": {
"DEBUG": "nsite,nsite:*", "DEBUG": "nsite,nsite:*"
"ALL_PROXY": "pac+file://proxy.pac"
} }
} }
] ]

View File

@ -39,10 +39,12 @@ COPY ./public ./public
COPY tor-and-i2p.pac proxy.pac COPY tor-and-i2p.pac proxy.pac
VOLUME [ "/var/cache/nginx" ] VOLUME [ "/var/cache/nginx" ]
VOLUME [ "/screenshots" ]
EXPOSE 80 3000 EXPOSE 80 3000
ENV NSITE_PORT="3000" ENV NSITE_PORT="3000"
ENV NGINX_CACHE_DIR="/var/cache/nginx" ENV NGINX_CACHE_DIR="/var/cache/nginx"
ENV SCREENSHOTS_DIR="/screenshots"
COPY docker-entrypoint.sh / COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh

View File

@ -1,5 +1,6 @@
version: "3.7" version: "3.7"
services: services:
nsite: nsite:
build: . build: .
@ -8,6 +9,10 @@ services:
LOOKUP_RELAYS: wss://user.kindpag.es,wss://purplepag.es LOOKUP_RELAYS: wss://user.kindpag.es,wss://purplepag.es
SUBSCRIPTION_RELAYS: wss://nostrue.com/,wss://nos.lol/,wss://relay.damus.io/,wss://purplerelay.com/ SUBSCRIPTION_RELAYS: wss://nostrue.com/,wss://nos.lol/,wss://relay.damus.io/,wss://purplerelay.com/
volumes: volumes:
- type: tmpfs
target: /screenshots
tmpfs:
size: 100M
- type: tmpfs - type: tmpfs
target: /var/cache/nginx target: /var/cache/nginx
tmpfs: tmpfs:

View File

@ -27,11 +27,13 @@
"keyv": "^5.0.1", "keyv": "^5.0.1",
"koa": "^2.15.3", "koa": "^2.15.3",
"koa-morgan": "^1.0.1", "koa-morgan": "^1.0.1",
"koa-send": "^5.0.1",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"mime": "^4.0.4", "mime": "^4.0.4",
"nostr-tools": "^2.7.2", "nostr-tools": "^2.7.2",
"pac-proxy-agent": "^7.0.2", "pac-proxy-agent": "^7.0.2",
"proxy-agent": "^6.4.0", "proxy-agent": "^6.4.0",
"puppeteer": "^23.5.0",
"websocket-polyfill": "^1.0.0", "websocket-polyfill": "^1.0.0",
"ws": "^8.18.0", "ws": "^8.18.0",
"xbytes": "^1.9.1" "xbytes": "^1.9.1"
@ -44,6 +46,7 @@
"@types/follow-redirects": "^1.14.4", "@types/follow-redirects": "^1.14.4",
"@types/koa": "^2.14.0", "@types/koa": "^2.14.0",
"@types/koa-morgan": "^1.0.8", "@types/koa-morgan": "^1.0.8",
"@types/koa-send": "^4.1.6",
"@types/koa-static": "^4.0.4", "@types/koa-static": "^4.0.4",
"@types/koa__cors": "^5.0.0", "@types/koa__cors": "^5.0.0",
"@types/koa__router": "^12.0.4", "@types/koa__router": "^12.0.4",

522
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,100 @@
import { html, css, LitElement } from "lit";
import { nip19 } from "nostr-tools";
import { pool, relays } from "../pool.js";
export class NsiteCard extends LitElement {
static styles = css`
:host {
min-width: 3in;
max-width: 4in;
border: 1px solid lightslategray;
display: flex;
flex-direction: column;
padding: 0.5em;
gap: 0.3em;
border-radius: 0.5em;
}
.title {
display: flex;
gap: 0.5em;
align-items: center;
color: initial;
text-decoration: none;
}
.title h3 {
margin: 0;
}
.avatar {
width: 3rem;
height: 3rem;
border: none;
outline: none;
border-radius: 50%;
}
.thumb {
display: flex;
overflow: hidden;
}
.thumb > img {
width: 100%;
border-radius: 0.5em;
}
.about {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
time {
margin-top: auto;
}
`;
static properties = {
nsite: { type: Object },
profile: { state: true, type: Object },
};
constructor() {
super();
}
connectedCallback() {
super.connectedCallback();
pool.get(relays, { kinds: [0], authors: [this.nsite.pubkey] }).then((event) => {
if (event) this.profile = JSON.parse(event.content);
});
}
render() {
const npub = nip19.npubEncode(this.nsite.pubkey);
const url = new URL("/", `${location.protocol}//${npub}.${location.host}`);
return html`
<a class="thumb" href="${url}" target="_blank">
<img src="/screenshot/${this.nsite.pubkey}.png" />
</a>
<a class="title" href="${url}" target="_blank">
${this.profile && html`<img src="${this.profile.image || this.profile.picture}" class="avatar" />`}
<div>
${this.profile
? html`
<h3>${this.profile.display_name || this.profile.name}</h3>
<small>${this.profile.nip05}</small>
`
: html`<h3>${npub.slice(0, 8)}</h3>`}
</div>
</a>
${this.profile && html`<p class="about">${this.profile.about}</p>`}
<time>${new Date(this.nsite.created_at * 1000).toDateString()}</time>
`;
}
}
customElements.define("nsite-card", NsiteCard);

View File

@ -5,16 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>nsite</title> <title>nsite</title>
<link rel="stylesheet" href="/lib/normalize.css" />
<link rel="stylesheet" href="/lib/milligram.css" />
<script src="/lib/nostr-name.js"></script>
<script src="/lib/nostr-picture.js"></script>
<script type="importmap"> <script type="importmap">
{ {
"imports": { "imports": {
"blossom-client-sdk": "https://esm.run/blossom-client-sdk", "blossom-client-sdk": "https://esm.run/blossom-client-sdk",
"nostr-tools": "https://esm.run/nostr-tools" "nostr-tools": "https://esm.run/nostr-tools",
"lit": "https://esm.run/lit",
"lit/directives/repeat.js": "https://esm.run/lit/directives/repeat.js"
} }
} }
</script> </script>
@ -24,25 +21,9 @@
width: 100%; width: 100%;
} }
</style> </style>
<script type="module" src="./main.js"></script>
</head> </head>
<body> <body>
<div class="container"> <nsite-app></nsite-app>
<img src="/logo.jpg" style="max-height: 2in" />
<h1>nsite</h1>
<a class="navbar-item" href="https://github.com/hzrd149/nsite-ts" target="_blank">Source Code</a>
<h2 class="subtitle is-2">Latest nsites:</h2>
<div id="sites" style="display: flex; flex-direction: column; gap: 0.5em"></div>
</div>
<template id="site">
<a class="card nsite-link" style="display: flex; flex-direction: column">
<nostr-picture style="max-width: 48px; max-height: 48px; overflow: hidden"></nostr-picture>
<nostr-name class="title is-4"></nostr-name>
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</a>
</template>
<script type="module" src="./main.js"></script>
</body> </body>
</html> </html>

29
public/lib/lit.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,58 @@
import { nip19, SimplePool } from "nostr-tools"; import { LitElement, html, css } from "lit";
import { repeat } from "lit/directives/repeat.js";
import "./components/nsite-card.js";
import { pool, relays } from "./pool.js";
const relays = ["wss://relay.damus.io", "wss://nos.lol", "wss://nostr.wine"]; export class NsiteApp extends LitElement {
const pool = new SimplePool(); static properties = {
selected: { state: true },
status: { state: true, type: String },
sites: { state: true, type: Array },
};
const seen = new Set(); static styles = css`
function addSite(event) { .sites {
if (seen.has(event.pubkey)) return; display: flex;
seen.add(event.pubkey); gap: 0.5em;
flex-wrap: wrap;
try {
const template = document.getElementById("site");
const site = template.content.cloneNode(true);
const npub = nip19.npubEncode(event.pubkey);
site.querySelector("nostr-name").setAttribute("pubkey", event.pubkey);
site.querySelector("nostr-name").textContent = npub.slice(0, 8);
site.querySelector("nostr-picture").setAttribute("pubkey", event.pubkey);
site
.querySelector(".nsite-link")
?.setAttribute("href", new URL("/", `${location.protocol}//${npub}.${location.host}`).toString());
site.querySelector("time").textContent = new Date(event.created_at * 1000).toDateString();
document.getElementById("sites").appendChild(site);
} catch (error) {
console.log("Failed to add site", event);
console.log(error);
} }
`;
seen = new Set();
constructor() {
super();
this.sites = [];
} }
console.log("Loading sites"); connectedCallback() {
super.connectedCallback();
pool.subscribeMany(relays, [{ kinds: [34128], "#d": ["/index.html"] }], { pool.subscribeMany(relays, [{ kinds: [34128], "#d": ["/index.html"] }], {
onevent: addSite, onevent: (event) => {
if (this.seen.has(event.pubkey)) return;
this.seen.add(event.pubkey);
this.sites = [...this.sites, event].sort((a, b) => b.created_at - a.created_at);
},
}); });
}
render() {
return html`<div class="container">
<img src="/logo.jpg" style="max-height: 2in" />
<h1>nsite</h1>
<a class="navbar-item" href="https://github.com/hzrd149/nsite-ts" target="_blank">Source Code</a>
<h2 class="subtitle is-2">Latest nsites:</h2>
<div class="sites">
${repeat(
this.sites,
(nsite) => nsite.pubkey,
(nsite) => html`<nsite-card .nsite="${nsite}"></nsite-card>`,
)}
</div>
</div>`;
}
}
customElements.define("nsite-app", NsiteApp);

4
public/pool.js Normal file
View File

@ -0,0 +1,4 @@
import { SimplePool } from "nostr-tools";
export const relays = ["wss://relay.damus.io", "wss://nos.lol", "wss://nostr.wine"];
export const pool = new SimplePool();

View File

@ -5,7 +5,10 @@ const LOOKUP_RELAYS = process.env.LOOKUP_RELAYS?.split(",").map((u) => u.trim())
"wss://user.kindpag.es/", "wss://user.kindpag.es/",
"wss://purplepag.es/", "wss://purplepag.es/",
]; ];
const SUBSCRIPTION_RELAYS = process.env.SUBSCRIPTION_RELAYS?.split(",").map((u) => u.trim()) ?? []; const SUBSCRIPTION_RELAYS = process.env.SUBSCRIPTION_RELAYS?.split(",").map((u) => u.trim()) ?? [
"wss://nos.lol",
"wss://relay.damus.io",
];
const BLOSSOM_SERVERS = process.env.BLOSSOM_SERVERS?.split(",").map((u) => u.trim()) ?? []; const BLOSSOM_SERVERS = process.env.BLOSSOM_SERVERS?.split(",").map((u) => u.trim()) ?? [];
const MAX_FILE_SIZE = process.env.MAX_FILE_SIZE ? xbytes.parseSize(process.env.MAX_FILE_SIZE) : Infinity; const MAX_FILE_SIZE = process.env.MAX_FILE_SIZE ? xbytes.parseSize(process.env.MAX_FILE_SIZE) : Infinity;
@ -17,6 +20,12 @@ const PAC_PROXY = process.env.PAC_PROXY;
const TOR_PROXY = process.env.TOR_PROXY; const TOR_PROXY = process.env.TOR_PROXY;
const I2P_PROXY = process.env.I2P_PROXY; const I2P_PROXY = process.env.I2P_PROXY;
const NSITE_HOST = process.env.NSITE_HOST || "0.0.0.0";
const NSITE_PORT = process.env.NSITE_PORT ? parseInt(process.env.NSITE_PORT) : 3000;
const HOST = `${NSITE_HOST}:${NSITE_PORT}`;
const SCREENSHOTS_DIR = process.env.SCREENSHOTS_DIR || "./screenshots";
export { export {
SUBSCRIPTION_RELAYS, SUBSCRIPTION_RELAYS,
LOOKUP_RELAYS, LOOKUP_RELAYS,
@ -27,4 +36,8 @@ export {
PAC_PROXY, PAC_PROXY,
TOR_PROXY, TOR_PROXY,
I2P_PROXY, I2P_PROXY,
NSITE_HOST,
NSITE_PORT,
HOST,
SCREENSHOTS_DIR,
}; };

View File

@ -2,21 +2,22 @@
import "./polyfill.js"; import "./polyfill.js";
import Koa from "koa"; import Koa from "koa";
import serve from "koa-static"; import serve from "koa-static";
import path from "node:path"; import path, { basename } from "node:path";
import cors from "@koa/cors"; import cors from "@koa/cors";
import fs from "node:fs"; import fs from "node:fs";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import mime from "mime"; import mime from "mime";
import morgan from "koa-morgan"; import morgan from "koa-morgan";
import send from "koa-send";
import { resolveNpubFromHostname } from "./helpers/dns.js"; import { resolveNpubFromHostname } from "./helpers/dns.js";
import { getNsiteBlobs, parseNsiteEvent } from "./events.js"; import { getNsiteBlobs, parseNsiteEvent } from "./events.js";
import { downloadFile, getUserBlossomServers } from "./blossom.js"; import { downloadFile, getUserBlossomServers } from "./blossom.js";
import { BLOSSOM_SERVERS, NGINX_CACHE_DIR, SUBSCRIPTION_RELAYS } from "./env.js"; import { BLOSSOM_SERVERS, HOST, NGINX_CACHE_DIR, NSITE_HOST, NSITE_PORT, SUBSCRIPTION_RELAYS } from "./env.js";
import { userDomains, userRelays, userServers } from "./cache.js"; import { userDomains, userRelays, userServers } from "./cache.js";
import { NSITE_KIND } from "./const.js";
import { invalidatePubkeyPath } from "./nginx.js"; import { invalidatePubkeyPath } from "./nginx.js";
import pool, { getUserOutboxes, subscribeForEvents } from "./nostr.js"; import pool, { getUserOutboxes, subscribeForEvents } from "./nostr.js";
import { getScreenshotPath, hasScreenshot, removeScreenshot, takeScreenshot } from "./screenshots.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -43,7 +44,7 @@ app.use(async (ctx, next) => {
} catch (err) { } catch (err) {
console.log(err); console.log(err);
ctx.status = 500; ctx.status = 500;
ctx.body = { message: "Something went wrong" }; if (err instanceof Error) ctx.body = { message: err.message };
} }
}); });
@ -84,6 +85,10 @@ app.use(async (ctx, next) => {
} }
} }
relays.push(...SUBSCRIPTION_RELAYS);
if (relays.length === 0) throw new Error("No nostr relays");
console.log(`${pubkey}: Searching for ${ctx.path}`); console.log(`${pubkey}: Searching for ${ctx.path}`);
const blobs = await getNsiteBlobs(pubkey, ctx.path, relays); const blobs = await getNsiteBlobs(pubkey, ctx.path, relays);
@ -145,27 +150,40 @@ try {
app.use(serve(www)); app.use(serve(www));
} }
app.listen( // get screenshots for websites
{ app.use(async (ctx, next) => {
port: process.env.NSITE_PORT || 3000, if (ctx.method === "GET" && ctx.path.startsWith("/screenshot")) {
host: process.env.NSITE_HOST || "0.0.0.0", const [pubkey, etx] = basename(ctx.path).split(".");
},
() => {
console.log("Started on port", process.env.PORT || 3000);
},
);
// invalidate nginx cache on new events if (pubkey) {
if (NGINX_CACHE_DIR && SUBSCRIPTION_RELAYS.length > 0) { if (!(await hasScreenshot(pubkey))) await takeScreenshot(pubkey);
await send(ctx, getScreenshotPath(pubkey));
} else throw Error("Missing pubkey");
} else return next();
});
app.listen({ host: NSITE_HOST, port: NSITE_PORT }, () => {
console.log("Started on port", HOST);
});
// invalidate nginx cache and screenshots on new events
if (SUBSCRIPTION_RELAYS.length > 0) {
console.log(`Listening for new nsite events`); console.log(`Listening for new nsite events`);
subscribeForEvents(SUBSCRIPTION_RELAYS, async (event) => { subscribeForEvents(SUBSCRIPTION_RELAYS, async (event) => {
try { try {
const nsite = parseNsiteEvent(event); const nsite = parseNsiteEvent(event);
if (nsite) { if (nsite) {
if (NGINX_CACHE_DIR) {
console.log(`${nsite.pubkey}: Invalidating ${nsite.path}`); console.log(`${nsite.pubkey}: Invalidating ${nsite.path}`);
await invalidatePubkeyPath(nsite.pubkey, nsite.path); await invalidatePubkeyPath(nsite.pubkey, nsite.path);
} }
// invalidate screenshot for nsite
if (nsite.path === "/" || nsite.path === "/index.html") {
await removeScreenshot(nsite.pubkey);
}
}
} catch (error) { } catch (error) {
console.log(`Failed to invalidate ${event.id}`); console.log(`Failed to invalidate ${event.id}`);
} }

40
src/screenshots.ts Normal file
View File

@ -0,0 +1,40 @@
import { nip19 } from "nostr-tools";
import puppeteer from "puppeteer";
import { join } from "path";
import pfs from "fs/promises";
import { NSITE_PORT, SCREENSHOTS_DIR } from "./env.js";
try {
await pfs.mkdir(SCREENSHOTS_DIR, { recursive: true });
} catch (error) {}
export function getScreenshotPath(pubkey: string) {
return join(SCREENSHOTS_DIR, pubkey + ".png");
}
export async function hasScreenshot(pubkey: string) {
try {
await pfs.stat(getScreenshotPath(pubkey));
return true;
} catch (error) {
return false;
}
}
export async function takeScreenshot(pubkey: string) {
console.log(`${pubkey}: Generating screenshot`);
const browser = await puppeteer.launch();
const page = await browser.newPage();
const url = new URL(`http://${nip19.npubEncode(pubkey)}.localhost:${NSITE_PORT}`);
await page.goto(url.toString());
await page.screenshot({ path: getScreenshotPath(pubkey) });
await browser.close();
}
export async function removeScreenshot(pubkey: string) {
try {
await pfs.rm(getScreenshotPath(pubkey));
} catch (error) {}
}