README corto y sin comentarios en el código

This commit is contained in:
hacklab 2026-09-29 10:26:46 +02:00
parent cb551e2a3c
commit 7ed0276704
9 changed files with 7 additions and 165 deletions

120
README.md
View file

@ -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 `<li>`
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 '<script|javascript:|\son[a-z]+=' site/ # nada
grep -rnoE 'https?://[^"]+' site/ # solo enlaces salientes
```
## Licencia
Código: AGPL-3.0 (`LICENSE`). Textos e imágenes: CC BY-SA 4.0.
Tipografía Anton: SIL OFL 1.1 (`site/fonts/OFL.txt`).
Código AGPL-3.0. Textos CC BY-SA 4.0. Tipografía Anton, OFL.

View file

@ -1,5 +1,3 @@
# Copiar a /etc/nginx/snippets/acampada26s-hub.conf
# OASIS escuchando en 127.0.0.1:3000 (node backend.js --public --no-open --host=127.0.0.1)
limit_except GET HEAD { deny all; }
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
@ -9,7 +7,6 @@ proxy_set_header X-Forwarded-For "";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
proxy_buffering on;
# add_header en location anula los del server: se repiten aquí todos menos la CSP.
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;

View file

@ -1,10 +1,5 @@
# acampada26s.net
# Estático en /var/www/acampada26s.net (contenido de site/).
# /c -> 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<ip>\d+\.\d+\.\d+)\.\d+ $ip.0;
~(?P<ip>[^:]+:[^:]+): $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; }
}

View file

@ -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

View file

@ -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; }

Binary file not shown.

View file

@ -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}`));

View file

@ -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 <!-- X:inicio --> y <!-- X:fin -->
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():

View file

@ -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