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.
193 lines
6.4 KiB
Python
Executable file
193 lines
6.4 KiB
Python
Executable file
#!/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()
|