tinyrelayfile
tinyrelay/scripts/qa/room-attachments-fixture.mjs/

scripts/qa/room-attachments-fixture.mjs raw
1#!/usr/bin/env node
2// 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";
11
12const 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 a
43// 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));

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs