diff --git a/README.md b/README.md index 7e4df7d..9683d02 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,11 @@ # acampada26s.net -Web de la acampada del 26S en Madrid. Comunicados, directo, cómo organizarse -y dónde están los colectivos. +Web de la acampada del 26S en Madrid. HTML y CSS, sin JavaScript. -HTML y CSS. Sin JavaScript en el navegador, sin cookies, sin analítica y sin -peticiones a terceros. La tipografía va dentro del repo. +1. `site/` va a `/var/www/acampada26s.net`. `deploy/nginx.conf` y `deploy/hub.conf` a nginx. +2. Directo es el HUB del pub OASIS (`/c`), servido en local en `127.0.0.1:3000`. +3. Comunicados, comisiones y debates salen de OASIS con `deploy/web26s.timer`. Rellenar `CUENTA` y `TRIBU` en `deploy/web26s.service`. +4. Publica quien está en la tribu pública de la acampada y lo sigue la cuenta de la acampada. Comunicado: post con `#comunicado26s`, primera línea de título. Comisión: subtribu pública. Debate: foro público. +5. Fotos subidas a mano: `tools/limpia-img.sh` antes, siempre. -La comunicación viva no depende de esta web: va por un pub de -[OASIS](https://code.03c8.net/krakenslab/oasis) (Secure Scuttlebutt). Si la web -cae o la tiran, el pub y lo publicado siguen en la red. - -## Estructura - -``` -site/ lo que se sirve (copiar tal cual a /var/www/acampada26s.net) -deploy/ nginx y timer de systemd -tools/ generador desde OASIS y limpieza de imágenes -``` - -## Qué sale de dónde - -- **Directo**: enlace al HUB del pub (`/c?type=feed`). Lo sube la gente de - comunicación desde OASIS. No pasa por esta web. -- **Comunicados, comisiones y debates**: los genera `tools/` a partir de OASIS - cada dos minutos. Nadie edita HTML a mano. -- **Colectivos**: lista fija en `site/index.html`. Para añadir uno, un `
  • ` - más. Solo webs o cuentas comprobadas. - -## Publicar desde OASIS - -Quien publica en la web: miembros de la tribu pública de la acampada **a los -que la cuenta de la acampada sigue**. Hacen falta las dos cosas. Unirse a una -tribu es un mensaje que cualquiera puede publicar sobre sí mismo (el código de -invitación solo lo comprueba la interfaz), así que la tribu sola no vale como -permiso. El follow lo firma la cuenta de la acampada y no se puede falsificar. - -- Dar permiso: invitar a la tribu y seguir desde la cuenta de la acampada. -- Quitarlo: dejar de seguir. Sus comunicados desaparecen en la siguiente pasada. - -Qué se publica: - -- **Comunicado**: un post con `#comunicado26s`. La primera línea es el título. - Las imágenes adjuntas salen en la página. Si se borra en OASIS, se borra aquí. -- **Comisión**: una subtribu pública de la tribu de la acampada, creada por - alguien con permiso. -- **Debate**: un foro público abierto por alguien con permiso. - -Las tribus privadas y lo que se escribe dentro de las tribus va cifrado; eso -no sale en la web ni debe salir. - -La tribu tiene que ser pública (no anónima) y con invitación estricta, y la -tiene que crear la cuenta de la acampada. - -## Imágenes - -Las de los comunicados las sirve el pub (`/c/blob/...`). Las que se suban a -mano a `site/img/`, siempre limpias antes: - -``` -tools/limpia-img.sh site/img/*.jpg -``` - -El EXIF lleva GPS, modelo de móvil y hora. Caras tapadas salvo permiso expreso. - -## Despliegue - -Requisitos: nginx, certbot, el pub OASIS en la misma máquina, Node 22 y -Python 3.9+. - -``` -rsync -a --exclude comunicados/ site/ /var/www/acampada26s.net/ -cp deploy/nginx.conf /etc/nginx/sites-available/acampada26s.net -cp deploy/hub.conf /etc/nginx/snippets/acampada26s-hub.conf -ln -s ../sites-available/acampada26s.net /etc/nginx/sites-enabled/ -nginx -t && systemctl reload nginx -``` - -Un `rsync` de `index.html` deja los bloques generados vacíos; el timer los -repone en dos minutos. - -### Pub OASIS - -`sh oasis.sh server`. El HUB web escucha en `:3000`; mejor solo en local -(`--host=127.0.0.1`). nginx publica `/c` en solo lectura y sin la IP de quien -visita. - -### Generador - -``` -cp -r . /opt/26s -cp deploy/web26s.service deploy/web26s.timer /etc/systemd/system/ -# rellenar CUENTA, TRIBU, OASIS_DIR y User en web26s.service -systemctl enable --now web26s.timer -``` - -A mano: - -``` -OASIS_DIR=~/oasis CUENTA=@...ed25519 TRIBU=%...sha256 node tools/extrae.js \ - | python3 tools/genera.py site -``` - -`extrae.js` solo lee del sbot por su socket local. `genera.py` escapa todo lo -que viene de SSB, solo deja enlaces `http(s)` y escribe de forma atómica. - -## Comprobaciones antes de subir - -``` -grep -rniE ' HUB clearnet de OASIS (solo lectura) del pub de la acampada. - server_tokens off; -# Logs sin IP completa: se guarda solo la /24. map $remote_addr $ip_anon { ~(?P\d+\.\d+\.\d+)\.\d+ $ip.0; ~(?P[^:]+:[^:]+): $ip::; @@ -48,7 +43,6 @@ server { expires 7d; } - # HUB de OASIS. Trae CSS inline, así que hub.conf redefine las cabeceras sin la CSP. location = /c { include /etc/nginx/snippets/acampada26s-hub.conf; } location ^~ /c/ { include /etc/nginx/snippets/acampada26s-hub.conf; } } diff --git a/deploy/web26s.service b/deploy/web26s.service index 5e0c8a7..80875c7 100644 --- a/deploy/web26s.service +++ b/deploy/web26s.service @@ -6,11 +6,9 @@ After=oasis.service Type=oneshot User=oasis Environment=OASIS_DIR=/home/oasis/oasis -# cuenta de la acampada y clave (%...sha256) de su tribu pública Environment=CUENTA= Environment=TRIBU= Environment=WEB=/var/www/acampada26s.net -# node tiene que estar en el PATH del servicio (o poner la ruta completa) ExecStart=/bin/sh -c 'set -e; node /opt/26s/tools/extrae.js > /tmp/26s.json; python3 /opt/26s/tools/genera.py "$WEB" < /tmp/26s.json' NoNewPrivileges=yes ProtectSystem=strict diff --git a/site/css/main.css b/site/css/main.css index b52a8a2..4fa0fe4 100644 --- a/site/css/main.css +++ b/site/css/main.css @@ -43,7 +43,6 @@ h1 { font-size: clamp(48px, 11vw, 120px); margin: 24px 0 16px; } h2 { font-size: 40px; margin: 48px 0 16px; border-bottom: var(--bd); padding-bottom: 6px; } h3 { font-size: 26px; margin: 0 0 6px; } -/* cuadros y botones */ .cuadros, .botonera { display: grid; gap: var(--gap); padding: 0; margin: 0; list-style: none; } .cuadros { grid-template-columns: repeat(3, 1fr); } .botonera { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); margin-top: var(--gap); } @@ -68,7 +67,6 @@ h3 { font-size: 26px; margin: 0 0 6px; } outline: none; } -/* comunicados */ .lista { list-style: none; margin: 0; padding: 0; } .lista > li { border-bottom: 1px solid var(--fg); padding: 20px 0; } .lista a:hover, .lista a:focus-visible { color: var(--acc); } @@ -97,7 +95,6 @@ footer { margin: 64px 0 24px; border-top: var(--bd); padding-top: 12px; font-siz .bt:hover, .bt:focus-visible { transform: none; } } -/* colectivos */ .colectivos { list-style: none; margin: 0 0 16px; padding: 0; columns: 2 280px; column-gap: 32px; } .colectivos li { break-inside: avoid; padding: 6px 0; border-bottom: 1px solid var(--fg); } .colectivos a { font-family: var(--tit); font-size: 22px; text-transform: uppercase; text-decoration: none; } diff --git a/tools/__pycache__/genera.cpython-311.pyc b/tools/__pycache__/genera.cpython-311.pyc new file mode 100644 index 0000000..935537c Binary files /dev/null and b/tools/__pycache__/genera.cpython-311.pyc differ diff --git a/tools/extrae.js b/tools/extrae.js index b3f4e0d..4759ff0 100755 --- a/tools/extrae.js +++ b/tools/extrae.js @@ -1,16 +1,4 @@ #!/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'); @@ -26,7 +14,6 @@ if (!CUENTA || !CUENTA.startsWith('@') || !TRIBU || !TRIBU.startsWith('%')) { 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']; @@ -44,9 +31,6 @@ const todos = (src) => new Promise((res, rej) => { 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) { @@ -98,7 +82,6 @@ ssbClient(ssbConfig.keys, { remote, caps: ssbConfig.caps }).then(async (ssb) => 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; @@ -115,7 +98,6 @@ ssbClient(ssbConfig.keys, { remote, caps: ssbConfig.caps }).then(async (ssb) => 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}`)); diff --git a/tools/genera.py b/tools/genera.py index beb9aee..961b16f 100755 --- a/tools/genera.py +++ b/tools/genera.py @@ -1,17 +1,4 @@ #!/usr/bin/env python3 -"""Escribe en la web lo que sale de tools/extrae.js. - -Lee el JSON por stdin y, dentro de SITE: - - rellena en index.html los bloques entre y - para X = comunicados, comisiones, debates - - crea comunicados/AAAA-MM-DD-xxxxxxxx.html por comunicado y borra los que - ya no están (borrados en OASIS o de alguien que ya no publica) - -Todo el texto que viene de SSB se escapa. Escritura atómica: nginx nunca sirve -un fichero a medias. - - node tools/extrae.js | python3 tools/genera.py /var/www/acampada26s.net -""" import hashlib import html import json @@ -40,7 +27,6 @@ def blob(ref): def plano(texto): - """Texto sin markdown de imágenes/enlaces ni la etiqueta, en una línea.""" texto = IMG.sub("", texto) texto = ENLACE.sub(r"\1", texto) texto = ETIQUETA.sub("", texto) @@ -48,7 +34,6 @@ def plano(texto): def partir(texto): - """Primera línea con contenido = título; el resto = cuerpo.""" lineas = ETIQUETA.sub("", texto).replace("\r", "").split("\n") for i, l in enumerate(lineas): t = plano(l).lstrip("# ").strip() @@ -58,7 +43,6 @@ def partir(texto): def cuerpo(texto): - """Markdown mínimo: párrafos, saltos, imágenes del pub y enlaces http(s).""" out = [] for parrafo in re.split(r"\n\s*\n", texto.replace("\r", "").strip()): if not parrafo.strip(): diff --git a/tools/limpia-img.sh b/tools/limpia-img.sh index ef902dc..e64d9f5 100755 --- a/tools/limpia-img.sh +++ b/tools/limpia-img.sh @@ -1,7 +1,4 @@ #!/bin/sh -# Quita metadatos (EXIF, GPS, modelo de móvil) de las imágenes antes de subirlas. -# Uso: tools/limpia-img.sh foto1.jpg foto2.png ... -# Necesita exiftool (libimage-exiftool-perl) o mat2. set -eu [ $# -gt 0 ] || { echo "uso: $0 imagen..." >&2; exit 1; } @@ -17,7 +14,6 @@ else exit 1 fi -# comprobación: no debe quedar GPS if command -v exiftool >/dev/null 2>&1; then if exiftool -q -q -if '$GPSLatitude' -p '$FileName' -- "$@" 2>/dev/null | grep .; then echo "ojo: quedan coordenadas GPS en las de arriba" >&2