Contenido desde OASIS y colectivos
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.
This commit is contained in:
parent
f78773bb87
commit
cb551e2a3c
13 changed files with 505 additions and 261 deletions
89
README.md
89
README.md
|
|
@ -1,6 +1,7 @@
|
||||||
# acampada26s.net
|
# acampada26s.net
|
||||||
|
|
||||||
Web de la acampada del 26S en Madrid. Comunicados, directo y cómo organizarse.
|
Web de la acampada del 26S en Madrid. Comunicados, directo, cómo organizarse
|
||||||
|
y dónde están los colectivos.
|
||||||
|
|
||||||
HTML y CSS. Sin JavaScript en el navegador, sin cookies, sin analítica y sin
|
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.
|
peticiones a terceros. La tipografía va dentro del repo.
|
||||||
|
|
@ -14,69 +15,95 @@ cae o la tiran, el pub y lo publicado siguen en la red.
|
||||||
```
|
```
|
||||||
site/ lo que se sirve (copiar tal cual a /var/www/acampada26s.net)
|
site/ lo que se sirve (copiar tal cual a /var/www/acampada26s.net)
|
||||||
deploy/ nginx y timer de systemd
|
deploy/ nginx y timer de systemd
|
||||||
tools/ limpiar imágenes y regenerar el directo
|
tools/ generador desde OASIS y limpieza de imágenes
|
||||||
```
|
```
|
||||||
|
|
||||||
## Publicar un comunicado
|
## Qué sale de dónde
|
||||||
|
|
||||||
1. Copiar `site/comunicados/2026-09-26-convocatoria.html` a
|
- **Directo**: enlace al HUB del pub (`/c?type=feed`). Lo sube la gente de
|
||||||
`site/comunicados/AAAA-MM-DD-titulo.html` y cambiar título, fecha y texto.
|
comunicación desde OASIS. No pasa por esta web.
|
||||||
2. Añadir un `<li>` arriba del todo en la lista `#comunicados` de `site/index.html`.
|
- **Comunicados, comisiones y debates**: los genera `tools/` a partir de OASIS
|
||||||
3. Fotos a `site/img/`, siempre limpias antes:
|
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
|
tools/limpia-img.sh site/img/*.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
El EXIF lleva GPS, modelo de móvil y hora. Además: caras tapadas salvo
|
El EXIF lleva GPS, modelo de móvil y hora. Caras tapadas salvo permiso expreso.
|
||||||
permiso expreso.
|
|
||||||
|
|
||||||
4. Toda imagen con `alt`, `width` y `height`.
|
|
||||||
|
|
||||||
## Despliegue
|
## Despliegue
|
||||||
|
|
||||||
Requisitos: nginx, certbot. Para el directo: el pub OASIS en la misma máquina,
|
Requisitos: nginx, certbot, el pub OASIS en la misma máquina, Node 22 y
|
||||||
Node 22 y Python 3.9+.
|
Python 3.9+.
|
||||||
|
|
||||||
```
|
```
|
||||||
rsync -a --delete site/ /var/www/acampada26s.net/
|
rsync -a --exclude comunicados/ site/ /var/www/acampada26s.net/
|
||||||
cp deploy/nginx.conf /etc/nginx/sites-available/acampada26s.net
|
cp deploy/nginx.conf /etc/nginx/sites-available/acampada26s.net
|
||||||
cp deploy/hub.conf /etc/nginx/snippets/acampada26s-hub.conf
|
cp deploy/hub.conf /etc/nginx/snippets/acampada26s-hub.conf
|
||||||
ln -s ../sites-available/acampada26s.net /etc/nginx/sites-enabled/
|
ln -s ../sites-available/acampada26s.net /etc/nginx/sites-enabled/
|
||||||
nginx -t && systemctl reload nginx
|
nginx -t && systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
`rsync --delete` borra el bloque directo generado; el timer lo repone en un minuto.
|
Un `rsync` de `index.html` deja los bloques generados vacíos; el timer los
|
||||||
|
repone en dos minutos.
|
||||||
|
|
||||||
### Pub OASIS
|
### Pub OASIS
|
||||||
|
|
||||||
El pub se arranca con `sh oasis.sh server` y el HUB web escucha en `:3000`.
|
`sh oasis.sh server`. El HUB web escucha en `:3000`; mejor solo en local
|
||||||
Conviene que escuche solo en local (`--host=127.0.0.1`): nginx publica `/c`
|
(`--host=127.0.0.1`). nginx publica `/c` en solo lectura y sin la IP de quien
|
||||||
en solo lectura (GET/HEAD) y le quita la IP del visitante.
|
visita.
|
||||||
|
|
||||||
Invite para la home: `sh oasis.sh invite 1000`, y pegarlo en `#organizate`.
|
### Generador
|
||||||
|
|
||||||
### Directo
|
|
||||||
|
|
||||||
`tools/ultimos.js` lee los últimos posts de la identidad de la acampada por el
|
|
||||||
socket local del sbot. `tools/directo.py` los escribe en `index.html` entre
|
|
||||||
`<!-- directo:inicio -->` y `<!-- directo:fin -->`, escapado y con escritura
|
|
||||||
atómica. Sigue siendo un fichero estático: aguanta mucha carga y no hace falta
|
|
||||||
JavaScript.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cp -r . /opt/26s
|
cp -r . /opt/26s
|
||||||
cp deploy/directo.service deploy/directo.timer /etc/systemd/system/
|
cp deploy/web26s.service deploy/web26s.timer /etc/systemd/system/
|
||||||
# editar FEED, OASIS_DIR y User en directo.service
|
# rellenar CUENTA, TRIBU, OASIS_DIR y User en web26s.service
|
||||||
systemctl enable --now directo.timer
|
systemctl enable --now web26s.timer
|
||||||
```
|
```
|
||||||
|
|
||||||
A mano:
|
A mano:
|
||||||
|
|
||||||
```
|
```
|
||||||
OASIS_DIR=~/oasis node tools/ultimos.js @feed.ed25519 5 | python3 tools/directo.py site/index.html
|
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
|
## Comprobaciones antes de subir
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=acampada26s: regenera el bloque DIRECTO de la home
|
|
||||||
After=oasis.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User=oasis
|
|
||||||
Environment=OASIS_DIR=/home/oasis/oasis
|
|
||||||
Environment=FEED=@CAMBIAR.ed25519
|
|
||||||
Environment=WEB=/var/www/acampada26s.net/index.html
|
|
||||||
# node tiene que estar en el PATH del servicio (o poner la ruta completa)
|
|
||||||
ExecStart=/bin/sh -c 'node /opt/26s/tools/ultimos.js "$FEED" 5 | python3 /opt/26s/tools/directo.py "$WEB"'
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
ProtectSystem=strict
|
|
||||||
ReadWritePaths=/var/www/acampada26s.net
|
|
||||||
PrivateTmp=yes
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=acampada26s: DIRECTO cada minuto
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnBootSec=1min
|
|
||||||
OnUnitActiveSec=1min
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
18
deploy/web26s.service
Normal file
18
deploy/web26s.service
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[Unit]
|
||||||
|
Description=acampada26s: comunicados, comisiones y debates desde OASIS
|
||||||
|
After=oasis.service
|
||||||
|
|
||||||
|
[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
|
||||||
|
ReadWritePaths=/var/www/acampada26s.net
|
||||||
|
PrivateTmp=yes
|
||||||
9
deploy/web26s.timer
Normal file
9
deploy/web26s.timer
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Unit]
|
||||||
|
Description=acampada26s: actualizar cada 2 minutos
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=1min
|
||||||
|
OnUnitActiveSec=2min
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="es">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self'; img-src 'self'; font-src 'self'; base-uri 'none'; form-action 'none'">
|
|
||||||
<meta name="referrer" content="no-referrer">
|
|
||||||
<title>Primer día en la plaza · Acampada 26S</title>
|
|
||||||
<meta name="description" content="Comunicados, directo y organización de la acampada del 26S en Madrid.">
|
|
||||||
<link rel="stylesheet" href="/css/main.css">
|
|
||||||
<link rel="preload" href="/fonts/anton.woff2" as="font" type="font/woff2" crossorigin>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header class="w">
|
|
||||||
<p><a class="bt volver" href="/">Acampada 26S</a></p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="w">
|
|
||||||
<article>
|
|
||||||
<h1>Primer día en la plaza</h1>
|
|
||||||
<p><time datetime="2026-09-27">27.09.2026</time> <span class="borrador">Borrador</span></p>
|
|
||||||
<p>Crónica de la primera jornada pendiente de aprobar en asamblea.</p>
|
|
||||||
<p>Las fotos se suben limpias de metadatos: <code>tools/limpia-img.sh</code>.</p>
|
|
||||||
</article>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="w">
|
|
||||||
Textos CC BY-SA 4.0 · Código AGPL-3.0 · <a href="https://gitea.laenre.net/hacklab/26s" rel="noopener noreferrer">Fuente</a>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -96,3 +96,11 @@ footer { margin: 64px 0 24px; border-top: var(--bd); padding-top: 12px; font-siz
|
||||||
.cuadro:hover, .cuadro:focus-visible,
|
.cuadro:hover, .cuadro:focus-visible,
|
||||||
.bt:hover, .bt:focus-visible { transform: none; }
|
.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; }
|
||||||
|
.colectivos a:hover, .colectivos a:focus-visible { color: var(--acc); }
|
||||||
|
.colectivos span { display: block; font-size: 14px; }
|
||||||
|
.lista strong { font-family: var(--tit); font-weight: 400; font-size: 22px; text-transform: uppercase; }
|
||||||
|
|
|
||||||
|
|
@ -23,44 +23,71 @@
|
||||||
<nav aria-label="Redes">
|
<nav aria-label="Redes">
|
||||||
<ul class="botonera">
|
<ul class="botonera">
|
||||||
<li><a class="bt" href="/c">OASIS</a></li>
|
<li><a class="bt" href="/c">OASIS</a></li>
|
||||||
<li><a class="bt" href="https://t.me/CAMBIAR" rel="noopener noreferrer">Telegram</a></li>
|
<li><a class="bt" href="https://0asis.net" rel="noopener noreferrer">Instalar OASIS</a></li>
|
||||||
<li><a class="bt" href="https://CAMBIAR/@acampada26s" rel="noopener noreferrer">Mastodon</a></li>
|
|
||||||
<li><a class="bt" href="https://www.instagram.com/CAMBIAR" rel="noopener noreferrer">Instagram</a></li>
|
|
||||||
<li><a class="bt" href="https://x.com/CAMBIAR" rel="noopener noreferrer">X</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="w">
|
<main class="w">
|
||||||
<!-- directo:inicio -->
|
|
||||||
<!-- directo:fin -->
|
|
||||||
|
|
||||||
<section id="comunicados">
|
<section id="comunicados">
|
||||||
<h2>Comunicados</h2>
|
<h2>Comunicados</h2>
|
||||||
<ol class="lista" reversed>
|
<!-- comunicados:inicio -->
|
||||||
<li>
|
<p>Todavía no hay comunicados.</p>
|
||||||
<time datetime="2026-09-27">27.09.2026</time> <span class="borrador">Borrador</span>
|
<!-- comunicados:fin -->
|
||||||
<h3><a href="/comunicados/2026-09-27-primer-dia.html">Primer día en la plaza</a></h3>
|
|
||||||
<p>Resumen de la primera jornada y de las asambleas convocadas.</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<time datetime="2026-09-26">26.09.2026</time> <span class="borrador">Borrador</span>
|
|
||||||
<h3><a href="/comunicados/2026-09-26-convocatoria.html">Convocatoria 26S</a></h3>
|
|
||||||
<p>Por qué acampamos, dónde y cómo sumarse.</p>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="organizate">
|
<section id="organizate">
|
||||||
<h2>Organízate</h2>
|
<h2>Organízate</h2>
|
||||||
<p>La acampada publica en su propio pub de <a href="/c">OASIS</a>, una red social libre sobre Secure Scuttlebutt. Sin servidores de empresa y sin cuentas que puedan cerrar desde fuera. Lo que se publica en la plaza aparece en <a href="/c?type=feed">Directo</a>.</p>
|
<p>La acampada se organiza en <a href="/c">OASIS</a>, una red social libre sobre Secure Scuttlebutt. No hay empresa detrás ni cuentas que se puedan cerrar desde fuera. Al instalarla entras solo en La Plaza, el pub común, y desde ahí llegas a la acampada.</p>
|
||||||
<h3>Entrar al pub</h3>
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>Instala OASIS en el ordenador o la APK en el móvil.</li>
|
<li>Instala OASIS desde <a href="https://0asis.net" rel="noopener noreferrer">0asis.net</a>, en el móvil o en el ordenador.</li>
|
||||||
<li>Abre OASIS, ve a <em>Invites</em> y pega este código:</li>
|
<li>Busca la tribu <strong>Acampada 26S</strong>.</li>
|
||||||
|
<li>Entra en la comisión que te toque y en los debates.</li>
|
||||||
</ol>
|
</ol>
|
||||||
<pre><code>acampada26s.net:8008:@CAMBIAR.ed25519~CAMBIAR</code></pre>
|
<p><a href="/c?type=feed">Directo</a> es lo que la acampada va subiendo al pub. Se ve sin instalar nada.</p>
|
||||||
<p>A partir de ahí puedes leer, publicar y hablar con el resto de la acampada aunque esta web se caiga.</p>
|
|
||||||
|
<h3>Comisiones</h3>
|
||||||
|
<!-- comisiones:inicio -->
|
||||||
|
<p>Las comisiones se crean como tribus dentro de OASIS.</p>
|
||||||
|
<!-- comisiones:fin -->
|
||||||
|
|
||||||
|
<h3>Debates</h3>
|
||||||
|
<!-- debates:inicio -->
|
||||||
|
<p>Los debates se abren como foros dentro de OASIS.</p>
|
||||||
|
<!-- debates:fin -->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="colectivos">
|
||||||
|
<h2>Colectivos</h2>
|
||||||
|
<p>Centros sociales y colectivos de Madrid. Aquí también hay información y asambleas.</p>
|
||||||
|
<ul class="colectivos">
|
||||||
|
<li><a href="https://csolarosa.net" rel="noopener noreferrer">CSO La Rosa</a> <span>Centro · Lavapiés</span></li>
|
||||||
|
<li><a href="https://3peces3.wordpress.com" rel="noopener noreferrer">CS Tres Peces Tres</a> <span>Centro · Lavapiés</span></li>
|
||||||
|
<li><a href="https://eskalerakarakola.org" rel="noopener noreferrer">Eskalera Karakola</a> <span>Centro · Lavapiés</span></li>
|
||||||
|
<li><a href="https://estaesunaplaza.blogspot.com" rel="noopener noreferrer">Esta es una Plaza</a> <span>Centro · Lavapiés</span></li>
|
||||||
|
<li><a href="https://latabacalera.net" rel="noopener noreferrer">CSA La Tabacalera</a> <span>Centro · Lavapiés</span></li>
|
||||||
|
<li><a href="https://ateneomaliciosa.net" rel="noopener noreferrer">Ateneo La Maliciosa</a> <span>Arganzuela</span></li>
|
||||||
|
<li><a href="https://www.instagram.com/evarganzuela/" rel="noopener noreferrer">EVA · Espacio Vecinal Arganzuela</a> <span>Arganzuela</span></li>
|
||||||
|
<li><a href="https://eslaeko.net" rel="noopener noreferrer">ESLA EKO</a> <span>Carabanchel</span></li>
|
||||||
|
<li><a href="https://asambleadecarabanchel.org" rel="noopener noreferrer">Asamblea Popular de Carabanchel</a> <span>Carabanchel</span></li>
|
||||||
|
<li><a href="https://ateneolibertariocarabanchellatina.wordpress.com" rel="noopener noreferrer">Ateneo Libertario Carabanchel-Latina</a> <span>Carabanchel</span></li>
|
||||||
|
<li><a href="https://www.laenre.net" rel="noopener noreferrer">CSOA La Enredadera</a> <span>Tetuán</span></li>
|
||||||
|
<li><a href="https://www.instagram.com/csolaanimosa/" rel="noopener noreferrer">CSO La Animosa</a> <span>Hortaleza</span></li>
|
||||||
|
<li><a href="https://www.lapiluka.org" rel="noopener noreferrer">CS La Piluka</a> <span>Fuencarral · Barrio del Pilar</span></li>
|
||||||
|
<li><a href="https://www.instagram.com/elsoldelaconce/" rel="noopener noreferrer">El Sol de la Conce</a> <span>Ciudad Lineal · La Concepción</span></li>
|
||||||
|
<li><a href="https://www.csseco.org" rel="noopener noreferrer">CS Seco</a> <span>Retiro</span></li>
|
||||||
|
<li><a href="https://lavillana.org" rel="noopener noreferrer">La Villana de Vallekas</a> <span>Puente de Vallecas</span></li>
|
||||||
|
<li><a href="https://www.ateneorepublicanodevallecas.org" rel="noopener noreferrer">Ateneo Republicano de Vallekas</a> <span>Puente de Vallecas</span></li>
|
||||||
|
<li><a href="https://apoyalacasika.wordpress.com" rel="noopener noreferrer">CSOA La Casika</a> <span>Móstoles</span></li>
|
||||||
|
</ul>
|
||||||
|
<h3>Vivienda y coordinadoras</h3>
|
||||||
|
<ul class="colectivos">
|
||||||
|
<li><a href="https://inquilinato.org" rel="noopener noreferrer">Sindicato de Inquilinas de Madrid</a></li>
|
||||||
|
<li><a href="https://x.com/pahmadrid" rel="noopener noreferrer">PAH Madrid</a></li>
|
||||||
|
<li><a href="https://coordinadoraviviendamadrid.com" rel="noopener noreferrer">Coordinadora de Vivienda de Madrid</a></li>
|
||||||
|
<li><a href="https://yayoflautasmadrid.org" rel="noopener noreferrer">Yayoflautas Madrid</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>¿Falta tu colectivo? Escribe a la comisión de comunicación en OASIS.</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Reescribe el bloque DIRECTO de index.html con los últimos posts del pub.
|
|
||||||
|
|
||||||
Lee de stdin el JSON de tools/ultimos.js y sustituye lo que haya entre
|
|
||||||
<!-- directo:inicio --> y <!-- directo:fin -->. Escritura atómica: el
|
|
||||||
servidor web nunca ve un fichero a medias.
|
|
||||||
|
|
||||||
node tools/ultimos.js @feed.ed25519 5 | python3 tools/directo.py /var/www/acampada26s.net/index.html
|
|
||||||
"""
|
|
||||||
import html
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
from datetime import datetime
|
|
||||||
from urllib.parse import quote
|
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
INI = "<!-- directo:inicio -->"
|
|
||||||
FIN = "<!-- directo:fin -->"
|
|
||||||
MAX = 400 # caracteres por post
|
|
||||||
TZ = ZoneInfo("Europe/Madrid")
|
|
||||||
|
|
||||||
|
|
||||||
def limpia(texto):
|
|
||||||
# fuera markdown de imágenes/enlaces y referencias ssb, queda el texto
|
|
||||||
texto = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", texto)
|
|
||||||
texto = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", texto)
|
|
||||||
texto = " ".join(texto.split())
|
|
||||||
if len(texto) > MAX:
|
|
||||||
texto = texto[:MAX].rsplit(" ", 1)[0] + "…"
|
|
||||||
return texto
|
|
||||||
|
|
||||||
|
|
||||||
def bloque(posts):
|
|
||||||
li = []
|
|
||||||
for p in posts:
|
|
||||||
t = datetime.fromtimestamp(p["ts"] / 1000, tz=TZ)
|
|
||||||
img = ""
|
|
||||||
if p.get("img"):
|
|
||||||
src = "/c/blob/" + quote(p["img"], safe="")
|
|
||||||
img = f'\n <img src="{html.escape(src)}" alt="" loading="lazy">'
|
|
||||||
li.append(
|
|
||||||
" <li>\n"
|
|
||||||
f' <time datetime="{t.isoformat(timespec="minutes")}">{t:%d.%m %H:%M}</time>{img}\n'
|
|
||||||
f" <p>{html.escape(limpia(p['text']))}</p>\n"
|
|
||||||
" </li>"
|
|
||||||
)
|
|
||||||
if not li:
|
|
||||||
return f"{INI}\n {FIN}"
|
|
||||||
return (
|
|
||||||
f'{INI}\n <section id="directo">\n <h2>Directo</h2>\n'
|
|
||||||
f' <ol class="lista">\n{chr(10).join(li)}\n </ol>\n'
|
|
||||||
f' <p><a href="/c?type=feed">Todo en OASIS</a></p>\n </section>\n {FIN}'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if len(sys.argv) != 2:
|
|
||||||
sys.exit("uso: ... | directo.py ruta/index.html")
|
|
||||||
ruta = sys.argv[1]
|
|
||||||
posts = json.load(sys.stdin)
|
|
||||||
if not isinstance(posts, list):
|
|
||||||
sys.exit("entrada no es una lista")
|
|
||||||
|
|
||||||
with open(ruta, encoding="utf-8") as f:
|
|
||||||
doc = f.read()
|
|
||||||
i, j = doc.find(INI), doc.find(FIN)
|
|
||||||
if i < 0 or j < i:
|
|
||||||
sys.exit("no están las marcas directo:inicio / directo:fin")
|
|
||||||
nuevo = doc[:i] + bloque(posts) + doc[j + len(FIN):]
|
|
||||||
if nuevo == doc:
|
|
||||||
return
|
|
||||||
|
|
||||||
d = os.path.dirname(os.path.abspath(ruta))
|
|
||||||
fd, tmp = tempfile.mkstemp(dir=d, prefix=".index.")
|
|
||||||
try:
|
|
||||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
||||||
f.write(nuevo)
|
|
||||||
os.chmod(tmp, 0o644)
|
|
||||||
os.replace(tmp, ruta)
|
|
||||||
except BaseException:
|
|
||||||
os.unlink(tmp)
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
161
tools/extrae.js
Executable file
161
tools/extrae.js
Executable file
|
|
@ -0,0 +1,161 @@
|
||||||
|
#!/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);
|
||||||
|
});
|
||||||
193
tools/genera.py
Executable file
193
tools/genera.py
Executable file
|
|
@ -0,0 +1,193 @@
|
||||||
|
#!/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
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import quote
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
TZ = ZoneInfo("Europe/Madrid")
|
||||||
|
ETIQUETA = re.compile(r"#comunicado26s\b", re.I)
|
||||||
|
IMG = re.compile(r"!\[[^\]]*\]\((&[A-Za-z0-9+/=]+\.sha256)\)")
|
||||||
|
ENLACE = re.compile(r"\[([^\]]+)\]\((https?://[^\s)]+)\)")
|
||||||
|
NOMBRE = re.compile(r"^\d{4}-\d{2}-\d{2}-[0-9a-f]{8}\.html$")
|
||||||
|
EXTRACTO = 240
|
||||||
|
|
||||||
|
|
||||||
|
def fecha(ts):
|
||||||
|
return datetime.fromtimestamp(ts / 1000, tz=TZ)
|
||||||
|
|
||||||
|
|
||||||
|
def blob(ref):
|
||||||
|
return "/c/blob/" + quote(ref, safe="")
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
return " ".join(texto.split())
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
if t:
|
||||||
|
return t[:120], "\n".join(lineas[i + 1:])
|
||||||
|
return "Comunicado", ""
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
||||||
|
continue
|
||||||
|
trozos, pos = [], 0
|
||||||
|
for m in re.finditer(f"{IMG.pattern}|{ENLACE.pattern}", parrafo):
|
||||||
|
trozos.append(html.escape(parrafo[pos:m.start()]))
|
||||||
|
if m.group(1):
|
||||||
|
trozos.append(f'<img src="{html.escape(blob(m.group(1)))}" alt="" loading="lazy">')
|
||||||
|
else:
|
||||||
|
trozos.append(f'<a href="{html.escape(m.group(3))}" rel="noopener noreferrer">'
|
||||||
|
f"{html.escape(m.group(2))}</a>")
|
||||||
|
pos = m.end()
|
||||||
|
trozos.append(html.escape(parrafo[pos:]))
|
||||||
|
p = "".join(trozos).strip().replace("\n", "<br>\n")
|
||||||
|
out.append(f" <p>{p}</p>")
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def nombre(c):
|
||||||
|
h = hashlib.sha256(c["key"].encode()).hexdigest()[:8]
|
||||||
|
return f"{fecha(c['ts']):%Y-%m-%d}-{h}.html"
|
||||||
|
|
||||||
|
|
||||||
|
def cambia(doc, clave, contenido):
|
||||||
|
ini, fin = f"<!-- {clave}:inicio -->", f"<!-- {clave}:fin -->"
|
||||||
|
i, j = doc.find(ini), doc.find(fin)
|
||||||
|
if i < 0 or j < i:
|
||||||
|
sys.exit(f"faltan las marcas {clave}:inicio / {clave}:fin")
|
||||||
|
return doc[:i + len(ini)] + "\n" + contenido + "\n " + doc[j:]
|
||||||
|
|
||||||
|
|
||||||
|
def escribe(ruta, texto):
|
||||||
|
try:
|
||||||
|
with open(ruta, encoding="utf-8") as f:
|
||||||
|
if f.read() == texto:
|
||||||
|
return
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(ruta), prefix=".tmp.")
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||||
|
f.write(texto)
|
||||||
|
os.chmod(tmp, 0o644)
|
||||||
|
os.replace(tmp, ruta)
|
||||||
|
except BaseException:
|
||||||
|
os.unlink(tmp)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def lista_comunicados(cs):
|
||||||
|
if not cs:
|
||||||
|
return " <p>Todavía no hay comunicados.</p>"
|
||||||
|
li = []
|
||||||
|
for c in cs:
|
||||||
|
t = fecha(c["ts"])
|
||||||
|
titulo, resto = partir(c["texto"])
|
||||||
|
ext = plano(resto)
|
||||||
|
if len(ext) > EXTRACTO:
|
||||||
|
ext = ext[:EXTRACTO].rsplit(" ", 1)[0] + "…"
|
||||||
|
li.append(
|
||||||
|
" <li>\n"
|
||||||
|
f' <time datetime="{t:%Y-%m-%d}">{t:%d.%m.%Y}</time>\n'
|
||||||
|
f' <h3><a href="/comunicados/{nombre(c)}">{html.escape(titulo)}</a></h3>\n'
|
||||||
|
+ (f" <p>{html.escape(ext)}</p>\n" if ext else "")
|
||||||
|
+ " </li>")
|
||||||
|
return ' <ol class="lista">\n' + "\n".join(li) + "\n </ol>"
|
||||||
|
|
||||||
|
|
||||||
|
def lista_simple(items, vacio, sub):
|
||||||
|
if not items:
|
||||||
|
return f" <p>{vacio}</p>"
|
||||||
|
li = []
|
||||||
|
for it in items:
|
||||||
|
s = sub(it)
|
||||||
|
li.append(f" <li><strong>{html.escape(it['titulo'])}</strong>"
|
||||||
|
+ (f" · {html.escape(s)}" if s else "") + "</li>")
|
||||||
|
return ' <ul class="lista">\n' + "\n".join(li) + "\n </ul>"
|
||||||
|
|
||||||
|
|
||||||
|
def pagina(plantilla, c):
|
||||||
|
t = fecha(c["ts"])
|
||||||
|
titulo, resto = partir(c["texto"])
|
||||||
|
art = (
|
||||||
|
"<article>\n"
|
||||||
|
f" <h1>{html.escape(titulo)}</h1>\n"
|
||||||
|
f' <p><time datetime="{t.isoformat(timespec="minutes")}">{t:%d.%m.%Y %H:%M}</time></p>\n'
|
||||||
|
f"{cuerpo(resto)}\n"
|
||||||
|
" </article>")
|
||||||
|
doc = cambia(plantilla, "articulo", " " + art)
|
||||||
|
return doc.replace("<title>Comunicado · Acampada 26S</title>",
|
||||||
|
f"<title>{html.escape(titulo)} · Acampada 26S</title>")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
sys.exit("uso: node tools/extrae.js | genera.py SITE")
|
||||||
|
site = sys.argv[1]
|
||||||
|
datos = json.load(sys.stdin)
|
||||||
|
cs = sorted(datos.get("comunicados", []), key=lambda c: c["ts"], reverse=True)
|
||||||
|
com = sorted(datos.get("comisiones", []), key=lambda c: c["titulo"].lower())
|
||||||
|
deb = sorted(datos.get("debates", []), key=lambda c: c["ts"], reverse=True)
|
||||||
|
|
||||||
|
idx = os.path.join(site, "index.html")
|
||||||
|
with open(idx, encoding="utf-8") as f:
|
||||||
|
doc = f.read()
|
||||||
|
doc = cambia(doc, "comunicados", lista_comunicados(cs))
|
||||||
|
doc = cambia(doc, "comisiones", lista_simple(
|
||||||
|
com, "Las comisiones se crean como tribus dentro de OASIS.",
|
||||||
|
lambda c: plano(c["texto"])[:EXTRACTO]))
|
||||||
|
doc = cambia(doc, "debates", lista_simple(
|
||||||
|
deb, "Los debates se abren como foros dentro de OASIS.",
|
||||||
|
lambda d: d["categoria"]))
|
||||||
|
escribe(idx, doc)
|
||||||
|
|
||||||
|
dcom = os.path.join(site, "comunicados")
|
||||||
|
os.makedirs(dcom, exist_ok=True)
|
||||||
|
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "plantilla.html"),
|
||||||
|
encoding="utf-8") as f:
|
||||||
|
plantilla = f.read()
|
||||||
|
quedan = set()
|
||||||
|
for c in cs:
|
||||||
|
n = nombre(c)
|
||||||
|
quedan.add(n)
|
||||||
|
escribe(os.path.join(dcom, n), pagina(plantilla, c))
|
||||||
|
for n in os.listdir(dcom):
|
||||||
|
if NOMBRE.match(n) and n not in quedan:
|
||||||
|
os.unlink(os.path.join(dcom, n))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -5,23 +5,19 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self'; img-src 'self'; font-src 'self'; base-uri 'none'; form-action 'none'">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self'; img-src 'self'; font-src 'self'; base-uri 'none'; form-action 'none'">
|
||||||
<meta name="referrer" content="no-referrer">
|
<meta name="referrer" content="no-referrer">
|
||||||
<title>Convocatoria 26S · Acampada 26S</title>
|
<title>Comunicado · Acampada 26S</title>
|
||||||
<meta name="description" content="Comunicados, directo y organización de la acampada del 26S en Madrid.">
|
<meta name="description" content="Comunicados, directo y organización de la acampada del 26S en Madrid.">
|
||||||
<link rel="stylesheet" href="/css/main.css">
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
<link rel="preload" href="/fonts/anton.woff2" as="font" type="font/woff2" crossorigin>
|
<link rel="preload" href="/fonts/anton.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="w">
|
<header class="w">
|
||||||
<p><a class="bt volver" href="/">Acampada 26S</a></p>
|
<a class="bt volver" href="/">Acampada 26S</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="w">
|
<main class="w">
|
||||||
<article>
|
<!-- articulo:inicio -->
|
||||||
<h1>Convocatoria 26S</h1>
|
<!-- articulo:fin -->
|
||||||
<p><time datetime="2026-09-26">26.09.2026</time> <span class="borrador">Borrador</span></p>
|
|
||||||
<p>Texto de la convocatoria pendiente de aprobar en asamblea.</p>
|
|
||||||
<p>Lugar, hora y motivos van aquí.</p>
|
|
||||||
</article>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="w">
|
<footer class="w">
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
// Saca los últimos posts de un feed SSB en JSON, para tools/directo.py.
|
|
||||||
// Se ejecuta en el servidor del pub, con el sbot de OASIS arrancado.
|
|
||||||
// Uso: OASIS_DIR=~/oasis node tools/ultimos.js @feed.ed25519 [N]
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const dir = process.env.OASIS_DIR || path.join(process.env.HOME, 'oasis');
|
|
||||||
const ssbConfig = require(path.join(dir, 'src', 'server', 'ssb_config'));
|
|
||||||
const ssbClient = require(path.join(dir, 'src', 'server', 'node_modules', 'ssb-client'));
|
|
||||||
|
|
||||||
const id = process.argv[2];
|
|
||||||
const n = Math.min(50, Math.max(1, parseInt(process.argv[3] || '5', 10)));
|
|
||||||
if (!id || !id.startsWith('@')) {
|
|
||||||
console.error('uso: node tools/ultimos.js @feed.ed25519 [N]');
|
|
||||||
process.exit(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((ssb) => {
|
|
||||||
const out = [];
|
|
||||||
const src = ssb.createUserStream({ id, reverse: true, limit: 200 });
|
|
||||||
const next = () => src(null, (end, msg) => {
|
|
||||||
if (end || out.length >= n) {
|
|
||||||
src(true, () => {});
|
|
||||||
ssb.close(() => console.log(JSON.stringify(out)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const c = msg.value && msg.value.content;
|
|
||||||
if (c && c.type === 'post' && typeof c.text === 'string') {
|
|
||||||
const img = (Array.isArray(c.mentions) ? c.mentions : [])
|
|
||||||
.find((m) => m && typeof m.link === 'string' && m.link.startsWith('&') &&
|
|
||||||
typeof m.type === 'string' && m.type.startsWith('image/'));
|
|
||||||
// OASIS no siempre pone type en mentions: se mira también el markdown
|
|
||||||
const md = c.text.match(/!\[[^\]]*\]\((&[A-Za-z0-9+/=]+\.sha256)\)/);
|
|
||||||
out.push({
|
|
||||||
key: msg.key,
|
|
||||||
ts: msg.value.timestamp,
|
|
||||||
text: c.text,
|
|
||||||
img: img ? img.link : (md ? md[1] : null),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
next();
|
|
||||||
}).catch((e) => {
|
|
||||||
console.error(e.message);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue