Skip to content

Repository files navigation

wp-to-nostr

WordPress-Termine als Nostr-Kalenderevents (kind:31923, NIP-52) veröffentlichen.

Holt Posts aus einer WordPress REST-API, mappt sie auf das NIP-52 Kalenderformat und publiziert sie auf einem Nostr-Relay. Läuft als GitHub Action (Cron alle 6 h) oder lokal mit Deno.

Features

  • Vollständige Pagination – holt alle Posts, nicht nur die erste Seite
  • Korrekte Zeitzonenkonvertierung – WordPress-Lokalzeit (Europe/Berlin) → UTC-Timestamp
  • HTML → Markdown – Content und Excerpt via Turndown
  • Addressable Eventsd-Tag = WordPress-Permalink, Relay ersetzt automatisch ältere Versionen
  • Relay-Deduplizierungcreated_at = modified_gmt aus WordPress → unveränderte Posts werden vom Relay automatisch ignoriert (kein unnötiger Write)
  • Kein Republish ohne inhaltliche Änderung – vor dem Publish wird der Event-Payload (Tags + Content) mit der Bestandsversion auf jedem Relay verglichen; ein reiner modified_gmt-Bump (WP-Bulk-Edit, Plugin-Save) löst kein Republish aus
  • Vergangene Termine werden übersprungen – Termine, deren Start/Ende bereits vorbei ist, werden nicht (erneut) publiziert und landen damit nicht als "frisch" in Client-Timelines
  • Single-Connection – alle Events über eine einzige WebSocket-Verbindung (statt pro Event eine neue)
  • Dry-Run-Modus – Events anzeigen ohne zu posten
  • Inspect-Tool – einzelne Posts debuggen mit Vergleichstabelle
  • Cleanup-Tool – alle Events per NIP-09 vom Relay löschen (für sauberen Neuaufbau)

Schnellstart

Voraussetzung

Deno ≥ 2.x installieren:

# macOS
brew install deno

# Linux / Windows
curl -fsSL https://deno.land/install.sh | sh

Dry Run (lokal testen, nichts posten)

deno task dry-run

Live (auf Relay veröffentlichen)

NOSTR_PRIVATE_KEY=nsec1… deno task start

Einzelnen Post inspizieren

deno task inspect              # erster Post
WP_PAGE=3 deno task inspect    # erster Post von Seite 3

Relay aufräumen (alle Events löschen)

# Dry-Run – zeigt was gelöscht würde
NOSTR_PRIVATE_KEY=nsec1… DRY_RUN=true deno task cleanup-dry

# Live – löscht alle kind:31923-Events per NIP-09
NOSTR_PRIVATE_KEY=nsec1… deno task cleanup

Hinweis zu strfry: NIP-09 Delete-Events werden von strfry persistent gespeichert. Nach einem Cleanup lehnt strfry Neu-Publikationen für gelöschte Adressen ab. Für einen sauberen Neuaufbau müssen die Delete-Events (kind:5) direkt auf dem Relay-Server entfernt werden – siehe docs/strfry-cleanup.md.

Umgebungsvariablen

Variable Pflicht Standard Beschreibung
NOSTR_PRIVATE_KEY Live-Modus ✅ nsec1… oder 64-stellige Hex-Zeichenkette
DRY_RUN false true → nur anzeigen, nicht posten
FORCE_REPUBLISH false truecreated_at = now, ersetzt rückwirkend alle Events einmalig
SYNC_MODE calendar calendar (kind:31923 Termine) oder article (kind:30023 Long-Form)
WP_API_URL https://relilab.org/wp-json/wp/v2/posts WordPress REST-API-Endpunkt
WP_CATEGORY 176 WordPress-Kategorie-ID (Termine: 176; Lernmodule: 6)
NOSTR_RELAYS 4 Relays (siehe Workflow) Komma-separierte Liste von Ziel-Relays. Singular NOSTR_RELAY wird zusätzlich akzeptiert (rückwärtskompatibel).
EXTRA_HASHTAGS "" (Workflow: relilab) Komma-separierte Hashtag-Liste, wird jedem Event als t-Tag angehängt, falls nicht ohnehin aus WordPress-Tags vorhanden. Case-insensitive Dedup.
COMMUNITY_NPUBS "" (Workflow: relilab-npub) Komma-separierte Liste von Community-Pubkeys (npub1… oder Hex), die als h-Tag (Communikey-Spec) an jedes Event angehängt werden.

Inkrementeller Sync

Beim Start fragt das Script jedes konfigurierte Relay nach dem neuesten eigenen Event des relevanten Kinds (31923 für Calendar, 30023 für Article) und nimmt das Minimum der created_at-Werte als Cutoff. Die WordPress-Query filtert dann mit modified_after, sodass nur seitdem geänderte Posts abgerufen und veröffentlicht werden.

Wann passiert ein Vollsync?

  • FORCE_REPUBLISH=true ist gesetzt
  • Mindestens ein Relay liefert kein eigenes Event (leeres Relay, neu hinzugefügt)
  • Mindestens ein Relay-REQ schlägt fehl oder läuft in den Timeout (5 s)

Schutz vor Timeline-Spam (zwei unabhängige Guards):

  1. Vergangene Termine: mapPostToCalendarEvent() überspringt Posts, deren Termin (Ende, ersatzweise Start) bereits vorbei ist. WP-Edits an alten Posts (z. B. Saison-Aufräumen) bumpen modified_gmt — ohne diesen Filter würden längst gelaufene Termine mit frischem created_at erneut oben in Client-Timelines auftauchen. Laufende Termine bleiben synchronisiert.
  2. Unveränderter Payload: Vor dem Publish holt das Script pro Relay die Bestandsversion jedes eigenen Events (d-Tag → neueste Version) und vergleicht Tags + Content (ohne created_at). Publiziert wird nur auf Relays, denen das Event fehlt oder deren Version inhaltlich abweicht. Hält ein Relay bereits eine neuere Version mit anderem Payload (z. B. aus einem früheren FORCE_REPUBLISH), wird created_at darüber angehoben, damit das Replace nicht mit „have newer" abgelehnt wird. Bei FORCE_REPUBLISH=true ist der Vergleich deaktiviert.

Was tun bei Mapping-Änderungen? Wenn der Mapping-Code (Tags, Content-Format, Hashtags) geändert wird, ändert sich modified_gmt in WordPress nicht — der Filter würde alle Bestandsposts ignorieren. Dann einmal mit FORCE_REPUBLISH=true per workflow_dispatch triggern: das holt alle Posts und ersetzt die Events mit created_at = now.

Tag-Anreicherung

Jedes Event wird vor dem Publish um zwei optionale Tag-Gruppen ergänzt:

  • t-Tags (Hashtags) aus EXTRA_HASHTAGS. Bestehende Hashtags aus den WordPress-Tags werden case-insensitive dedupliziert (Relilab aus WP blockt relilab aus der Konfig).
  • h-Tags (Community-Zuordnung nach Communikey-Spec) aus COMMUNITY_NPUBS. npubs werden beim Start zu Hex aufgelöst; pro Community ein h-Tag.

Beide Defaults leben in .github/workflows/sync.yml (relilab-spezifisch) — der Code-Default ist leer. Forks setzen entweder die Repo-Variablen WP_EXTRA_HASHTAGS / WP_COMMUNITY_NPUBS oder editieren den Workflow.

Hinweis zur Sichtbarkeit: Die Communikey-Spec setzt voraus, dass das Ziel-Relay nicht im enforced-Modus läuft, oder dass der Sync-npub auf der relevanten Profile-List der Community steht. Bei enforced-Relays ohne Eintrag werden Events sonst stumm verworfen. Aktuell trifft das auf relay-rpi.edufeed.org nicht zu.

Altbestand: Bestehende Events erhalten den neuen Tag erst, wenn der zugehörige WordPress-Post wieder gespeichert wird (modified_gmt steigt). Für sofortigen Rewrite aller Events cleanup-relay.ts + Re-Sync nutzen.

GitHub Actions

Es laufen zwei voneinander unabhängige Workflows alle 6 Stunden:

  • .github/workflows/sync.yml — Termine-Sync (SYNC_MODE=calendar, WP_CATEGORY=176, kind:31923). Cron-Offset: :00.
  • .github/workflows/sync-articles.yml — Article-Sync (SYNC_MODE=article, WP_CATEGORY=6 Lernmodule, kind:30023 Long-Form). Cron-Offset: :30.

Beide nutzen denselben Sync-npub (NOSTR_PRIVATE_KEY-Secret), unterscheiden sich nur durch Env-Variablen.

Einrichtung

  1. Secret anlegen: Repository → Settings → Secrets → Actions → NOSTR_PRIVATE_KEY
  2. Optional – Variables: WP_API_URL, WP_CATEGORY (Termine), WP_ARTICLE_CATEGORY (Beiträge), WP_NOSTR_RELAYS, WP_EXTRA_HASHTAGS, WP_COMMUNITY_NPUBS als Repository-Variables setzen, um die Defaults zu überschreiben
  3. Manueller Test: Actions → „WordPress → Nostr Sync" / „WordPress → Nostr Article Sync" → „Run workflow" → Dry Run = true
  4. Live schalten: Workflow erneut starten mit Dry Run = false

Beide Cron-Jobs laufen automatisch im Live-Modus (DRY_RUN=false).

Article-Sync: Autorenzuschreibung

Long-Form-Articles erhalten am Anfang des Markdown-Contents einen Header-Block, der WP-Autor*in und Quelle sichtbar macht:

> Erstellt von: [Corinna Ullmann](https://relilab.org/author/colibri/)
> Veröffentlicht auf [relilab.org](https://relilab.org/lernmodul-test/)

(eigentlicher Content folgt …)

Solange keine eigenen Autoren-npubs existieren, signiert der Bot-npub. Die Zuschreibung im Text bleibt sichtbar und kann später durch echte pubkey-Zuordnung ersetzt werden.

Projektstruktur

wp-to-nostr/
├── wp-to-nostr.ts              # Haupt-Sync-Script
├── inspect-mapping.ts          # Debug: einzelnen Post inspizieren
├── cleanup-relay.ts            # NIP-09: alle Events vom Relay löschen
├── deno.json                   # Tasks & Import-Map
├── .github/workflows/sync.yml  # GitHub Actions Workflow
├── docs/
│   └── nostr-kind-31923.md     # NIP-52 Mapping-Dokumentation
├── LICENSE
└── README.md

Mapping-Übersicht

WordPress → Nostr kind:31923
post.link d-Tag + r-Tag
title.rendered title-Tag
acf.relilab_startdate start-Tag (UTC Unix-Timestamp)
acf.relilab_enddate end-Tag (UTC Unix-Timestamp)
(fest: Europe/Berlin) start_tzid / end_tzid
excerpt.rendered summary-Tag (Markdown)
content.rendered content (Markdown)
acf.relilab_custom_zoom_link location-Tag
featured_image_urls_v2.thumbnail image-Tag
taxonomy_info.post_tag[].label t-Tags
modified_gmt created_at (Relay-Deduplizierung)

Relay-Deduplizierung via created_at

created_at wird auf den modified_gmt-Wert aus WordPress gesetzt (Unix-Timestamp). Da kind:31923 ein adressierbares ersetzbares Event ist (NIP-33), gilt:

  • Post unverändertmodified_gmt gleich → created_at gleich → Relay ignoriert das Event
  • Post bearbeitetmodified_gmt steigt → created_at höher → Relay ersetzt das Event

Für sehr alte Posts (deren modified_gmt vor 2025 liegt) greift ein fester Floor-Wert (2025-01-01T00:00:00Z), da viele Relays Events mit zu altem created_at ablehnen.

Detaillierte Spezifikation: docs/nostr-kind-31923.md

Anpassen für andere WordPress-Instanzen

  1. WP_API_URL auf deinen Endpunkt setzen
  2. WP_CATEGORY anpassen (oder Parameter entfernen)
  3. ACF-Feldnamen in mapPostToNostrEvent() an dein Schema anpassen
  4. Zeitzone in wpDateToUnix() und den _tzid-Tags ändern falls nötig

Lizenz

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages