// Group chats (White Noise) in the real UI, against the Rust reference client. // // bun tools/marmot-chat-test.js // // Same prerequisites as tools/marmot-interop-test.js (a local relay and a // running `wn` daemon); skips cleanly without them. Every relay socket the // page opens is pointed at the local relay, so nothing leaves the machine. // // Against a deployed site instead (real relays, throwaway identities): // SITE=https://v3.coinos.io RELAY=wss://relay.coinos.io bun tools/marmot-chat-test.js // with the wn daemon started on public relays (e.g. wss://nos.lol). // // Covers: becoming invitable (KeyPackage published at boot), a wn group // invitation → card → Join → messages both ways; "New message" to a White // Noise user founding a direct group whose replies land in the DM thread; // "New group" from the app. import puppeteer from './puppeteer.js'; import { spawnSync } from 'node:child_process'; import { generateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { buildHtml } from '../build.js'; import { seedPubkey } from '../src/nostr.js'; import * as nip06 from 'nostr-tools/nip06'; import { finalizeEvent } from 'nostr-tools/pure'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { mkdtempSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'; import { randomBytes, bytesToHex } from '@noble/hashes/utils'; // a tiny real PNG with a few random bytes after it, so every run's file is new const png = () => new Uint8Array([...Uint8Array.from(atob('iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFklEQVR4nGP8z8DAwMDAxMDAwMDAAAANHQEDasKb6QAAAABJRU5ErkJggg=='), (c) => c.charCodeAt(0)), ...randomBytes(8)]); const imgShown = () => page.evaluate(() => [...document.querySelectorAll('.chat-log img.note-img')].filter((i) => i.complete && i.naturalWidth > 0 && i.src.startsWith('blob:')).length); // shots go somewhere every machine has const shot = (name) => join(tmpdir(), name); const RELAY = process.env.RELAY || 'ws://127.0.0.1:27777'; const SITE = process.env.SITE || ''; const PORT = 5247; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); let ok = true; const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; function wn(args, account) { const r = spawnSync('wn', ['--json', ...(account ? ['--account', account] : []), ...args], { encoding: 'utf8' }); const line = (r.stdout || '').trim().split('\n').filter(Boolean).at(-1) || ''; try { const j = JSON.parse(line); return j.ok ? j.result : { error: j.error }; } catch { return { error: { message: (r.stderr || 'no output').trim() } }; } } function relayQuery(filter) { return new Promise((resolve) => { const ws = new WebSocket(RELAY), out = []; const done = () => { try { ws.close(); } catch {} resolve(out); }; ws.onopen = () => ws.send(JSON.stringify(['REQ', 'q', filter])); ws.onmessage = (m) => { const d = JSON.parse(m.data); if (d[0] === 'EVENT') out.push(d[2]); if (d[0] === 'EOSE') done(); }; ws.onerror = done; setTimeout(done, 3000); }); } const until = async (fn, ms = SITE ? 45000 : 20000) => { const end = Date.now() + ms; for (;;) { const v = await fn(); if (v) return v; if (Date.now() > end) return null; await sleep(500); } }; if (wn(['whoami']).error) { console.log('SKIP: wn daemon not reachable'); process.exit(0); } if (!(await relayQuery({ kinds: [0], limit: 1 }))) { console.log('SKIP: no relay'); process.exit(0); } const alice = wn(['create-identity']).account_id, bob = wn(['create-identity']).account_id; const server = SITE ? null : Bun.serve({ port: PORT, fetch: ((html) => () => new Response(html, { headers: { 'content-type': 'text/html' } }))(await buildHtml({ minify: true, pwa: false })), }); const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); const page = await browser.newPage(); await page.setViewport({ width: 1100, height: 850 }); page.on('pageerror', (e) => { console.log(' page error:', e.message); ok = false; }); // DEBUG=1 turns on the app's debug switch and echoes the group-chat lines if (process.env.DEBUG) { await page.evaluateOnNewDocument(() => localStorage.setItem('coinos-debug', '1')); page.on('console', async (m) => { if (!/^marmot/.test(m.text())) return; const args = await Promise.all(m.args().map((a) => a.evaluate((v) => (v instanceof Error ? v.message : typeof v === 'string' ? v : JSON.stringify(v))).catch(() => '?'))); console.log(' [page]', args.join(' ').slice(0, 300)); }); } // every relay is the local relay if (!SITE) await page.evaluateOnNewDocument((relay) => { const Real = window.WebSocket; window.WebSocket = function (url, protocols) { return new Real(/^wss?:/.test(url) ? relay : url, protocols); }; window.WebSocket.prototype = Real.prototype; Object.assign(window.WebSocket, { CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3 }); localStorage.setItem('coinos-group-relays', JSON.stringify([relay])); localStorage.setItem('btc-wallet-network', 'regtest'); }, RELAY); const click = (sel, t) => page.evaluate((s, x) => { const e = [...document.querySelectorAll(s)].find((n) => n.textContent.trim().toLowerCase().includes(x.toLowerCase())); if (e) { e.click(
let config = require('./config');
let fs = require('fs');
let mailgun = require('mailgun-js')({apiKey: config.mailgun, domain: config.domain });
let data = {
...config.mail,
subject: 'User Documents',
text: 'Documents uploaded',
};
fs.readdir('./uploads', (err, files) => {
files.forEach(file => {
console.log(file);
data.text = file;
fs.rename(file, '../verified' + file);
mailgun.messages().send(data, function (error, body) {
console.log(body);
});
});
});