clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / scripts / qa / room-attachments-fixture.mjs
room-attachments-fixture.mjs 4.9K
1#!/usr/bin/env node2// Seed a disposable members-only room with image and generic-file messages.3// The owner key is the public QA key used by the other scripts in this folder.4import { mkdir, writeFile, readFile } from "node:fs/promises";5import { execFile } from "node:child_process";6import { promisify } from "node:util";7import { resolve, join } from "node:path";8import { createHash } from "node:crypto";9import { finalizeEvent, getPublicKey } from "nostr-tools/pure";10import { npubEncode } from "nostr-tools/nip19";1112const args = process.argv.slice(2);13const option = (key, fallback) => args.includes(key) ? args[args.indexOf(key) + 1] : fallback;14const base = option("--relay", "http://127.0.0.1:18459").replace(/\/$/, "");15const dataDir = resolve(option("--data", "/tmp/tinyrelay-room-attachments"));16const outDir = resolve(option("--out", "output/playwright/room-attachments"));17const secret = Uint8Array.from(Buffer.from("fc1d06a0fd5e622dcf448d0b3c2fccc891a5ecf74546a7675460d92441fecfdd", "hex"));18const agentSecret = Uint8Array.from(Buffer.from("4f6d7a2be7f2c6f677b1dc2d02f4388cbd1b9aeb8e5e47d41e4ae0f2eb3bde5f", "hex"));19const owner = getPublicKey(secret), agent = getPublicKey(agentSecret), room = "attachments-qa";20const json = value => JSON.stringify(value);21const now = () => Math.floor(Date.now() / 1000);22const exec = promisify(execFile);23async function signed(path, method, value, signer = secret, contentType = "application/json") {24 const body = Buffer.isBuffer(value) ? value : Buffer.from(json(value));25 const payload = createHash("sha256").update(body).digest("hex");26 const auth = finalizeEvent({kind: 27235, created_at: now(), tags: [["u", base + path], ["method", method], ["payload", payload]], content: ""}, signer);27 const token = "Nostr " + Buffer.from(json(auth)).toString("base64");28 const response = await fetch(base + path, {method, headers: {authorization: token, "content-type": contentType}, body});29 const result = await response.json().catch(() => ({}));30 if (!response.ok && !(method === "POST" && path === "/events" && String(result.message || "").startsWith("duplicate:"))) throw Error(`${method} ${path}: ${response.status} ${json(result)}`);31 return result;32}33async function publish(event) { return signed("/events", "POST", event); }34async function upload(bytes, type, filename) {35 const hash = createHash("sha256").update(bytes).digest("hex");36 const descriptor = await signed(`/rooms/${room}/attachments?filename=${encodeURIComponent(filename)}`, "PUT", bytes, secret, type);37 if (descriptor.sha256 !== hash || descriptor.size !== bytes.length) throw Error("fixture upload descriptor mismatch");38 return descriptor;39}40await mkdir(dataDir, {recursive: true});41await mkdir(outDir, {recursive: true});42// Tenant ownership grants management access but event writes still require a43// community membership record in a fresh disposable tenant.44await signed("/manage/rpc", "POST", {method: "setmember", params: [owner, {role: "member"}]});45const create = finalizeEvent({kind: 9007, created_at: now(), tags: [["h", room], ["name", "Attachment QA"], ["about", "Disposable room for browser attachment checks."], ["visibility", "members"]], content: ""}, secret);46await publish(create);47await signed("/manage/rpc", "POST", {method: "setmember", params: [agent, {role: "member"}]});48await publish(finalizeEvent({kind: 9000, created_at: now(), tags: [["h", room], ["p", agent, "member"]], content: ""}, secret));49const cardPath = join(outDir, "fixture-card.png");50await exec("/opt/homebrew/bin/convert", ["-size", "320x160", "gradient:#244f3b-#9bd4b1", "-fill", "#10261e", "-stroke", "#d7ffe6", "-strokewidth", "3", "-draw", "roundrectangle 15,15 305,145 12,12", cardPath]);51const png = await readFile(cardPath);52await writeFile(join(outDir, "browser-upload.png"), png);53const image = await upload(png, "image/png", "fixture.png");54const text = Buffer.from("Attachment QA generic file\n");55await writeFile(join(outDir, "only.txt"), Buffer.from("only attachment"));56const file = await upload(text, "text/plain", "fixture notes.txt");57const message = (descriptor, signer = secret, content = "") => finalizeEvent({kind: 9, created_at: now(), tags: [["h", room], ["imeta", `url ${descriptor.url}`, `m ${descriptor.type}`, `x ${descriptor.sha256}`, `size ${descriptor.size}`, `filename ${descriptor.filename}`]], content: `${content ? content + "\n\n" : ""}${descriptor.type.startsWith("image/") ? `![image](${descriptor.url})` : `[${descriptor.filename}](${descriptor.url})`}`}, signer);58const initial = message(image, secret, "Initial image attachment");59await publish(initial);60const agentMessage = message(file, agentSecret, "Agent published file");61await signed("/events", "POST", agentMessage, agentSecret);62const manifest = {schema: 1, relay: base, dataDir, room, owner, ownerNpub: npubEncode(owner), agent, image, file, initial: initial.id, agentMessage: agentMessage.id};63await writeFile(join(outDir, "fixture.json"), json(manifest) + "\n");64console.log(json(manifest));