| 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. |
| 4 | import { mkdir, writeFile, readFile } from "node:fs/promises" ; |
| 5 | import { execFile } from "node:child_process" ; |
| 6 | import { promisify } from "node:util" ; |
| 7 | import { resolve, join } from "node:path" ; |
| 8 | import { createHash } from "node:crypto" ; |
| 9 | import { finalizeEvent, getPublicKey } from "nostr-tools/pure" ; |
| 10 | import { npubEncode } from "nostr-tools/nip19" ; |
| 11 | |
| 12 | const args = process.argv.slice(2); |
| 13 | const option = (key, fallback) => args.includes(key) ? args[args.indexOf(key) + 1] : fallback; |
| 14 | const base = option("--relay" , "http://127.0.0.1:18459" ).replace(/\/$/, "" ); |
| 15 | const dataDir = resolve(option("--data" , "/tmp/tinyrelay-room-attachments" )); |
| 16 | const outDir = resolve(option("--out" , "output/playwright/room-attachments" )); |
| 17 | const secret = Uint8Array.from(Buffer.from("fc1d06a0fd5e622dcf448d0b3c2fccc891a5ecf74546a7675460d92441fecfdd" , "hex" )); |
| 18 | const agentSecret = Uint8Array.from(Buffer.from("4f6d7a2be7f2c6f677b1dc2d02f4388cbd1b9aeb8e5e47d41e4ae0f2eb3bde5f" , "hex" )); |
| 19 | const owner = getPublicKey(secret), agent = getPublicKey(agentSecret), room = "attachments-qa" ; |
| 20 | const json = value => JSON.stringify(value); |
| 21 | const now = () => Math.floor(Date.now() / 1000); |
| 22 | const exec = promisify(execFile); |
| 23 | async 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 | } |
| 33 | async function publish(event) { return signed("/events" , "POST" , event); } |
| 34 | async 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 | } |
| 40 | await mkdir(dataDir, {recursive: true}); |
| 41 | await 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. |
| 44 | await signed("/manage/rpc" , "POST" , {method: "setmember" , params: [owner, {role: "member" }]}); |
| 45 | const create = finalizeEvent({kind: 9007, created_at: now(), tags: [["h" , room], ["name" , "Attachment QA" ], ["about" , "Disposable room for browser attachment checks." ], ["visibility" , "members" ]], content: "" }, secret); |
| 46 | await publish(create); |
| 47 | await signed("/manage/rpc" , "POST" , {method: "setmember" , params: [agent, {role: "member" }]}); |
| 48 | await publish(finalizeEvent({kind: 9000, created_at: now(), tags: [["h" , room], ["p" , agent, "member" ]], content: "" }, secret)); |
| 49 | const cardPath = join(outDir, "fixture-card.png" ); |
| 50 | await 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]); |
| 51 | const png = await readFile(cardPath); |
| 52 | await writeFile(join(outDir, "browser-upload.png" ), png); |
| 53 | const image = await upload(png, "image/png" , "fixture.png" ); |
| 54 | const text = Buffer.from("Attachment QA generic file\n" ); |
| 55 | await writeFile(join(outDir, "only.txt" ), Buffer.from("only attachment" )); |
| 56 | const file = await upload(text, "text/plain" , "fixture notes.txt" ); |
| 57 | const 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/") ? ` ` : ` [${descriptor.filename}](${descriptor.url})`}` }, signer); |
| 58 | const initial = message(image, secret, "Initial image attachment" ); |
| 59 | await publish(initial); |
| 60 | const agentMessage = message(file, agentSecret, "Agent published file" ); |
| 61 | await signed("/events" , "POST" , agentMessage, agentSecret); |
| 62 | const manifest = {schema: 1, relay: base, dataDir, room, owner, ownerNpub: npubEncode(owner), agent, image, file, initial: initial.id, agentMessage: agentMessage.id}; |
| 63 | await writeFile(join(outDir, "fixture.json" ), json(manifest) + "\n" ); |
| 64 | console.log(json(manifest)); |