#!/usr/bin/env node const path = require('path'); const dir = process.env.OASIS_DIR || path.join(process.env.HOME, 'oasis'); const CUENTA = process.env.CUENTA; const TRIBU = process.env.TRIBU; const ETIQUETA = 'comunicado26s'; if (!CUENTA || !CUENTA.startsWith('@') || !TRIBU || !TRIBU.startsWith('%')) { console.error('faltan CUENTA=@... y TRIBU=%...'); process.exit(1); } const ssbConfig = require(path.join(dir, 'src', 'server', 'ssb_config')); const ssbClient = require(path.join(dir, 'src', 'server', 'node_modules', 'ssb-client')); const ESTRUCTURA = ['title', 'description', 'image', 'location', 'tags', 'isAnonymous', 'inviteMode', 'status', 'parentTribeId', 'mapUrl']; const todos = (src) => new Promise((res, rej) => { const out = []; const leer = () => src(null, (end, m) => { if (end === true) return res(out); if (end) return rej(end); out.push(m); leer(); }); leer(); }); const lista = (x) => (Array.isArray(x) ? x : []); const igual = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null); function puntaTribu(raiz, msgs) { const porPadre = new Map(); for (const m of msgs) { const r = m.value.content.replaces; if (!r) continue; if (!porPadre.has(r)) porPadre.set(r, []); porPadre.get(r).push(m); } const autorRaiz = raiz.value.author; let punta = raiz; for (;;) { const prev = punta.value.content; const validos = lista(porPadre.get(punta.key)).filter((m) => { const c = m.value.content; const a = m.value.author; if (a === autorRaiz) return true; const antes = lista(prev.members); const despues = lista(c.members); if (!antes.includes(a) && !despues.includes(a)) return false; const altas = despues.filter((x) => !antes.includes(x)); const bajas = antes.filter((x) => !despues.includes(x)); const delta = altas.length + bajas.length === 0 || (altas.length === 1 && bajas.length === 0 && altas[0] === a) || (bajas.length === 1 && altas.length === 0 && bajas[0] === a); if (!delta) return false; const inv = new Set(lista(prev.invites).map((i) => i && i.codeHash)); if (lista(c.invites).some((i) => i && i.codeHash && !inv.has(i.codeHash))) return false; return ESTRUCTURA.every((f) => igual(prev[f], c[f])); }); if (!validos.length) return punta; validos.sort((x, y) => (y.value.author === autorRaiz) - (x.value.author === autorRaiz) || x.value.timestamp - y.value.timestamp || (x.key < y.key ? -1 : 1)); punta = validos[0]; } } function imagenes(texto) { const re = /!\[[^\]]*\]\((&[A-Za-z0-9+/=]+\.sha256)\)/g; return [...texto.matchAll(re)].map((m) => m[1]); } const pub = (ssbConfig.keys.public || '').replace('.ed25519', ''); const remote = `unix:${path.join(ssbConfig.path, 'socket')}~noauth:${pub}`; ssbClient(ssbConfig.keys, { remote, caps: ssbConfig.caps }).then(async (ssb) => { const tipo = (type) => todos(ssb.messagesByType({ type })); const [contactos, tribus, posts, feeds, foros, lapidas] = await Promise.all( ['contact', 'tribe', 'post', 'feed', 'forum', 'tombstone'].map(tipo)); const sigue = new Map(); for (const m of contactos) { const c = m.value.content; if (m.value.author !== CUENTA || typeof c.contact !== 'string') continue; if (typeof c.following === 'boolean') sigue.set(c.contact, c.following && !c.blocking); else if (c.blocking) sigue.set(c.contact, false); } const raiz = tribus.find((m) => m.key === TRIBU); if (!raiz || raiz.value.author !== CUENTA || raiz.value.content.isAnonymous !== false) { throw new Error('TRIBU no existe, no es pública o no la creó CUENTA'); } const deTribu = (r) => tribus.filter((m) => m.value.content.replaces).concat([r]); const miembros = lista(puntaTribu(raiz, deTribu(raiz)).value.content.members); const editores = new Set([CUENTA, ...miembros.filter((id) => sigue.get(id) === true)]); const borrado = new Set(lapidas .filter((m) => typeof m.value.content.target === 'string') .map((m) => `${m.value.author} ${m.value.content.target}`)); const vivo = (m) => !borrado.has(`${m.value.author} ${m.key}`); const deEditor = (m) => editores.has(m.value.author) && vivo(m); const lleva = (c) => (typeof c.text === 'string' && c.text.toLowerCase().includes(`#${ETIQUETA}`)) || lista(c.tags).some((t) => String(t).toLowerCase().replace(/^#/, '') === ETIQUETA); const comunicados = posts.concat(feeds) .filter((m) => deEditor(m) && !m.value.content.root && lleva(m.value.content)) .map((m) => ({ key: m.key, autor: m.value.author, ts: m.value.timestamp, texto: m.value.content.text, imgs: imagenes(m.value.content.text), })); const comisiones = tribus .filter((m) => !m.value.content.replaces && m.value.content.parentTribeId === TRIBU && m.value.content.isAnonymous === false && deEditor(m)) .map((r) => { const c = puntaTribu(r, deTribu(r)).value.content; return { key: r.key, ts: r.value.timestamp, titulo: c.title || '', texto: c.description || '' }; }); const debates = foros .filter((m) => typeof m.value.content === 'object' && !m.value.content.root && !m.value.content.isPrivate && deEditor(m)) .map((m) => ({ key: m.key, ts: m.value.timestamp, titulo: m.value.content.title || '', categoria: m.value.content.category || '', })); ssb.close(() => console.log(JSON.stringify({ comunicados, comisiones, debates }))); }).catch((e) => { console.error(e.message); process.exit(1); });