Comunicados, comisiones y debates salen del pub: los publica quien es miembro de la tribu de la acampada y además sigue la cuenta de la acampada. Directo enlaza al HUB. Lista de centros sociales y colectivos de Madrid con enlaces comprobados.
161 lines
6.5 KiB
JavaScript
Executable file
161 lines
6.5 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
// Lee del sbot de OASIS lo que va a la web y lo saca en JSON para tools/genera.py.
|
|
//
|
|
// Quién publica: miembros de la tribu TRIBU a los que CUENTA sigue, más CUENTA.
|
|
// La tribu sola no basta: en SSB cualquiera puede publicar que se une a una tribu
|
|
// (la invitación solo la comprueba la interfaz de OASIS). El follow de CUENTA sí
|
|
// está firmado por CUENTA.
|
|
//
|
|
// comunicados posts con #comunicado26s de quien publica
|
|
// comisiones subtribus públicas de TRIBU creadas por quien publica
|
|
// debates foros públicos abiertos por quien publica
|
|
//
|
|
// Uso: OASIS_DIR=~/oasis CUENTA=@...ed25519 TRIBU=%...sha256 node tools/extrae.js
|
|
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'));
|
|
|
|
// igual que STRUCTURAL_FIELDS en src/models/tribes_model.js
|
|
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);
|
|
|
|
// Cadena de una tribu (create + updates por `replaces`) con las mismas reglas que
|
|
// buildTribeIndex: el autor raíz cambia lo que quiera; otro miembro solo puede
|
|
// entrar o salir él mismo. Devuelve el último estado válido.
|
|
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));
|
|
|
|
// a quién sigue CUENTA (el último contact gana)
|
|
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)]);
|
|
|
|
// borrado: tombstone del propio autor
|
|
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);
|
|
});
|