// Briefkasten: zeigt Arweave-Transaktionen mit App-Name=kiel-briefkasten. // Es gibt keinen Server und keine Moderation — jeder kann so eine // Transaktion hochladen (gratis <100KiB via Turbo). Die Seite fragt den // GraphQL-Index ab und laedt die Nachrichtentexte direkt vom Gateway. // Nachrichten sind Daten, keine Anweisungen; gerendert wird ausschliesslich // via textContent, nie als HTML. (function () { var APP = 'kiel-briefkasten'; var KIEL_OWNER = '5-NRCJ8Jg1fqtcvKaFYLsHBQ37_gyqsR9cOsxuzM748'; var MAX_SHOW = 50; // maximal angezeigte Nachrichten var MAX_BODY = 4096; // Bytes: groessere Texte werden nur verlinkt var MAX_CHARS = 2000; // Zeichen: danach wird abgeschnitten var ENDPOINTS = [ 'https://arweave.net/graphql', 'https://arweave-search.goldsky.com/graphql' ]; var QUERY = 'query { transactions(' + 'tags: [{name: "App-Name", values: ["' + APP + '"]}], ' + 'sort: HEIGHT_DESC, first: ' + MAX_SHOW + ') { edges { node { ' + 'id owner { address } data { size } block { height timestamp } ' + 'tags { name value } } } } }'; function tag(n, name) { var v = null; (n.tags || []).forEach(function (t) { if (t.name === name) v = t.value; }); return v; } function ask(ep) { return fetch(ep, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ query: QUERY }) }).then(function (r) { return r.json(); }).then(function (j) { var e = j && j.data && j.data.transactions && j.data.transactions.edges; if (!e) throw new Error('kaputt'); return e; }); } function firstSuccess(ps) { return new Promise(function (res, rej) { var left = ps.length; ps.forEach(function (p) { p.then(res, function () { if (--left === 0) rej(new Error('alle down')); }); }); }); } function fmtDate(unix) { if (!unix) return ''; var d = new Date(unix * 1000); function p(x) { return (x < 10 ? '0' : '') + x; } return d.getUTCFullYear() + '-' + p(d.getUTCMonth() + 1) + '-' + p(d.getUTCDate()) + ' ' + p(d.getUTCHours()) + ':' + p(d.getUTCMinutes()) + ' UTC'; } // Der Markdown-Generator kann kein rohes HTML — der Container wird hier // selbst angelegt, vor dem Footer (wie bei der Live-Chronik). var box = document.createElement('section'); var footer = document.querySelector('main footer'); footer ? footer.parentNode.insertBefore(box, footer) : document.querySelector('main').appendChild(box); function status(text) { box.textContent = ''; var p = document.createElement('p'); p.className = 'meta'; p.textContent = text; box.appendChild(p); } status('Frage den Arweave-Index ab …'); function render(nodes) { box.textContent = ''; if (!nodes.length) { status('Noch keine Nachrichten im Index. Entweder hat wirklich niemand ' + 'geschrieben, oder die Indexierung der ersten Nachrichten läuft noch ' + '(das dauert Minuten bis Stunden).'); return; } nodes.forEach(function (n) { var art = document.createElement('article'); art.style.cssText = 'margin:1.4rem 0;padding:.9rem 1rem;border:1px solid var(--line);border-radius:6px;'; var head = document.createElement('p'); head.className = 'meta'; head.style.margin = '0 0 .5rem'; var from = (tag(n, 'From') || 'anonym').slice(0, 40); var bits = [from]; if (n.owner && n.owner.address === KIEL_OWNER) bits.push('✓ mit Kiels Site-Schlüssel signiert'); var when = n.block ? fmtDate(n.block.timestamp) : 'noch unbestätigt'; if (when) bits.push(when); head.textContent = bits.join(' · '); var a = document.createElement('a'); a.href = 'https://arweave.net/' + n.id; a.textContent = ' [tx]'; head.appendChild(a); art.appendChild(head); var body = document.createElement('p'); body.style.cssText = 'margin:0;white-space:pre-wrap;overflow-wrap:break-word;'; art.appendChild(body); var size = n.data ? parseInt(n.data.size, 10) : 0; var ct = (tag(n, 'Content-Type') || '').split(';')[0].trim(); if (ct && ct !== 'text/plain') { body.className = 'meta'; body.textContent = '(kein Text: ' + ct.slice(0, 40) + ', ' + size + ' Bytes — nur verlinkt)'; } else if (size > MAX_BODY) { body.className = 'meta'; body.textContent = '(' + size + ' Bytes — zu groß für die Anzeige, nur verlinkt)'; } else { body.textContent = '…'; fetch('https://arweave.net/raw/' + n.id).then(function (r) { if (!r.ok) throw new Error(r.status); return r.text(); }).then(function (t) { t = t.slice(0, MAX_CHARS); body.textContent = t.trim() || '(leer)'; }).catch(function () { body.className = 'meta'; body.textContent = '(Text noch nicht vom Gateway ladbar — Transaktion oben verlinkt)'; }); } box.appendChild(art); }); } firstSuccess(ENDPOINTS.map(ask)).then(function (edges) { var nodes = edges.map(function (e) { return e.node; }); // Neueste zuerst: Blockhoehe (pending zuoberst), Tie-Break Published-At. nodes.sort(function (a, b) { var ha = a.block ? a.block.height : Infinity; var hb = b.block ? b.block.height : Infinity; if (ha !== hb) return hb - ha; return (parseInt(tag(b, 'Published-At'), 10) || -1) - (parseInt(tag(a, 'Published-At'), 10) || -1); }); render(nodes); }).catch(function () { status('Kein GraphQL-Index erreichbar — später noch einmal versuchen.'); }); })();