Skip to content

Latest commit

 

History

History
451 lines (334 loc) · 13.6 KB

File metadata and controls

451 lines (334 loc) · 13.6 KB

yaml parsen mit gray-matter und mappen für nostr

Dieses Tutorial zeigt, wie du sowohl Markdown-Dateien mit YAML-Frontmatter als auch JSON-Dateien zu Nostr Kind-30023 Events konvertierst und diese in das Nostr-Netzwerk postest. Das erweiterte Mapping unterstützt sowohl grundlegende als auch komplexe Bildungsmetadaten.

Verfügbare Skripte

Das Projekt umfasst folgende Hauptskripte:

Skript Beschreibung
parse.js Einfache Umwandlung von Markdown/YAML zu JSON
adv-mapping.js Erweitertes Mapping von Markdown/YAML oder JSON zu Nostr-Events
post.js Veröffentlichung von Nostr-Events an konfigurierte Relays
fetch-and-post.js Abruf und Veröffentlichung von Remote-Markdown-Dateien
bulk-fetch-post.js Bulk-Verarbeitung mehrerer Markdown-Dateien aus Git-Repositories

Erste Schritte

Installation

  1. Klone dieses Repository:

    git clone https://github.com/edufeed-org/md2nostr-tool.git
    cd md2nostr-tool
  2. Installiere alle Abhängigkeiten:

    npm install
  3. Erstelle eine .env-Datei basierend auf .env.example:

    cp .env.example .env
  4. Bearbeite die .env-Datei und füge deinen Nostr-Schlüssel und gewünschte Relays hinzu.

Einfacher Workflow-Test

  1. Erstelle eine Testdatei test.md mit YAML-Frontmatter:

    ---
    title: Test-Artikel
    summary: Eine kurze Zusammenfassung
    tags:
      - test
      - beispiel
    ---
    
    # Test-Inhalt
    
    Dies ist ein Testinhalt.
  2. Konvertiere die Markdown-Datei zu einem Nostr-Event:

    node adv-mapping.js test.md > test-mapped.json
  3. Veröffentliche das Event an die Relays:

    node post.js test-mapped.json

1. Parsing

Paket: npm gray-matter als Bibliothek für YAML -> JSON

install ( für .md nötig) npm i gray-matter

Skript parse.js

const fs = require("fs");
const matter = require("gray-matter");

const file = fs.readFileSync("test.md", "utf8");
const parsed = matter(file);


console.log(JSON.stringify(event));

Erklärungen zum Code:

const fs = require("fs");
  • Lädt das Node-Core-Modul fs (Dateisystem). Damit kannst du Dateien lesen/schreiben.
const matter = require("gray-matter");
  • Lädt gray-matter (vorher npm i gray-matter). matter() parst Frontmatter aus Strings.
const { data, content } = matter(fs.readFileSync("beispiel.md", "utf8"));
  • fs.readFileSync("beispiel.md", "utf8"): Liest synchron die Datei als UTF-8-String (kein Buffer).
  • matter(<string>): Trennt Frontmatter vom Body und gibt ein Objekt zurück.
  • Destructuring: Nimmt daraus direkt data (YAML als Objekt) und content (Markdown-Body als String).
console.log(JSON.stringify({ ...data, content: content.trim() }, null, 2));
  • Baut ein neues Objekt: ...data (alle YAML-Felder) + content (Body, mit trim() führende/abschließende Leerzeilen weg).
  • JSON.stringify(obj, null, 2): Pretty-Print mit 2 Leerzeichen Einrückung.
  • console.log(...): Gibt das eine JSON-Objekt auf STDOUT aus.

Ausführung:

node parse.js artikel.md > artikel-konvertiert.json

2. Mapping nach Nostr Kind 30023

Grundlegende Mapping-Regeln

YAML-Feld Nostr-Tag Beschreibung
url / id d Eindeutige Identifier für das Event
name / title title Titel des Artikels
summary / description summary Kurze Zusammenfassung (optional mit Sprache)
image / cover.image / thumbnailUrl image URL zum Titelbild
datePublished / published / date published_at Veröffentlichungsdatum (Unix-Timestamp)
tags t Array von Tags (jeder als eigener t-Tag)
content / body content Der Markdown-Inhalt

Erweiterte Bildungsmetadaten-Mapping

YAML-Feld Nostr-Tag Beschreibung
about about Themen/Konzepte (URI, Label, Sprache)
learningResourceType learningResourceType Art der Lernressource
keywords keywords Schlüsselwörter (kompakt in einem Tag)
inLanguage / language inLanguage Sprachen der Ressource
license license Lizenzinformationen
educationalLevel / educationLevel educationalLevel Bildungsniveau
creator creator, creatorId, creatorAffiliation Autor/in-Informationen

Erweiterte Mapping-Funktionalitäten

Das erweiterte Mapping-Skript adv-mapping.js bietet:

  • Multi-Format-Support: Verarbeitet sowohl .json als auch .md Dateien
  • Flexible Feldmapping: Unterstützt alternative Feldnamen (z.B. name oder title)
  • Sprachunterstützung: Mehrsprachige Inhalte mit inLanguage-Tags
  • Bildungsmetadaten: Spezialisierte Tags für Lernressourcen
  • Robuste Verarbeitung: Fehlerbehandlung und Validierung

Erweiterte Mapping-Funktion adv-mapping.js

// adv-mapping.js
// Usage:
//   node adv-mapping.js input.json
//   node adv-mapping.js input.md
//
// Für .md: npm i gray-matter

const fs = require("fs");
let matter = null;
try { matter = require("gray-matter"); } catch {}

function parseInput(filePath) {
  const raw = fs.readFileSync(filePath, "utf8");
  const isMD = /\.md$/i.test(filePath);
  if (!isMD) return JSON.parse(raw);

  if (!matter) throw new Error("gray-matter fehlt. Bitte `npm i gray-matter` installieren.");
  const fm = matter(raw);
  const data = fm.data || {};
  const content = (fm.content || "").trim();
  return { ...data, content };
}

function mapTo30023(src = {}) {
  if (!src || typeof src !== "object") return { error: "invalid input" };

  const content = (src.content || src.body || "").toString().trim();
  if (!content) return { error: "missing content" };

  const now = Math.floor(Date.now() / 1000);
  const tags = [];

  // Flexible Feldmapping
  const url = src.url || src.id;
  const name = src.name || src.title;
  const summary = src.summary || src.description;
  
  // Grundlegende Tags
  if (url) tags.push(["d", String(url)]);
  if (name) tags.push(["title", String(name)]);
  if (summary) tags.push(["summary", String(summary)]);
  
  // Bild-URL aus verschiedenen Quellen
  const image = src.image || src.cover?.image || src.thumbnailUrl;
  if (image) tags.push(["image", String(image)]);
  
  // Datum-Verarbeitung
  const published = src.datePublished || src.published || src.date;
  if (published) {
    const ts = Math.floor(new Date(published).getTime() / 1000);
    if (!Number.isNaN(ts)) tags.push(["published_at", String(ts)]);
  }
  
  // Standard-Tags
  if (Array.isArray(src.tags)) {
    for (const t of src.tags.filter(Boolean)) {
      tags.push(["t", String(t)]);
    }
  }
  
  // Erweiterte Bildungsmetadaten
  // ... (weitere Mappings für about, learningResourceType, etc.)
  
  return {
    kind: 30023,
    created_at: now,
    content,
    tags
  };
}

// Hauptfunktion
const inputFile = process.argv[2] || "oeroep.json";
try {
  const src = parseInput(inputFile);
  const event = mapTo30023(src);
  if (event.error) {
    console.error(`Fehler: ${event.error}`);
    process.exit(1);
  }
  console.log(JSON.stringify(event, null, 2));
} catch (err) {
  console.error("Fehler beim Verarbeiten:", err.message);
  process.exit(1);
}

Nutzung

Das erweiterte Skript kann sowohl JSON- als auch Markdown-Dateien verarbeiten:

# JSON-Datei verarbeiten
node adv-mapping.js artikel-konvertiert.json > artikel-mapped.json

# Markdown-Datei direkt verarbeiten (benötigt gray-matter)
node adv-mapping.js artikel.md > artikel-mapped.json

# Mit verschiedenen Eingabedateien
node adv-mapping.js oeroep.json > oeroep-mapped.json
node adv-mapping.js beispiel.md > beispiel-mapped.json

Workflow-Optionen:

  1. Direkter MD-zu-Nostr Workflow:

    node adv-mapping.js artikel.md > artikel-mapped.json
  2. Traditioneller 2-Schritt Workflow:

    node parse.js artikel.md > artikel-konvertiert.json
    node adv-mapping.js artikel-konvertiert.json > artikel-mapped.json

to nostr posten

PrelayInit fehlt in deiner nostr-tools-Version. Nimm den einfachsten, versionssicheren Weg mit SimplePool (fire-and-forget) – keine ACKs, aber zuverlässig genug fürs „simpel posten“.

Install

npm i nostr-tools ws dotenv

Die dotenv-Bibliothek ermöglicht das Laden von Umgebungsvariablen aus einer .env-Datei, was sicherer als Umgebungsvariablen in der Befehlszeile ist.

.env-Datei erstellen

Erstelle eine .env-Datei im Projektverzeichnis mit folgendem Inhalt:

# Nostr-Schlüssel (entweder nsec oder hex)
NSEC=nsec1...
# NOSTR_SK=dein_hex_secret_key  # Alternative zu NSEC

# Komma-getrennte Liste der Relays
RELAYS=wss://relay-rpi.edufeed.org/,wss://relay.damus.io,wss://relay.primal.net/,wss://nostr.win

Die .env-Datei sollte niemals in die Versionskontrolle (Git) eingecheckt werden! Füge .env zu deiner .gitignore-Datei hinzu.

post.js (CommonJS, minimal)

const fs = require("fs");
// dotenv-Konfiguration laden
require("dotenv").config();
const { finalizeEvent, getPublicKey, nip19, SimplePool } = require("nostr-tools");

// WebSocket-Impl für Node
global.WebSocket = require("ws");

// kleine Helper
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

(async () => {
  try {
    const inPath = process.argv[2] || "nostr-schrein.json";
    // .env Datei nutzen oder Fallback-Werte
    const relayList = (process.env.RELAYS || "wss://nos.lol,wss://relay.damus.io")
      .split(",").map(s => s.trim()).filter(Boolean);

    const secret = process.env.NSEC || process.env.NOSTR_SK; // nsec... oder hex
    if (!secret) throw new Error("Bitte NSEC oder NOSTR_SK in .env-Datei oder als Umgebungsvariable setzen.");

    const sk = secret.startsWith("nsec") ? nip19.decode(secret).data : secret;
    const pk = getPublicKey(sk);

    const src = JSON.parse(fs.readFileSync(inPath, "utf8"));
    if (src.kind !== 30023) throw new Error("JSON muss kind: 30023 enthalten.");
    if (!src.content) throw new Error("JSON braucht content.");

    const unsigned = { ...src, pubkey: pk, created_at: src.created_at || Math.floor(Date.now()/1000) };
    const ev = finalizeEvent(unsigned, sk); // id + sig

    const pool = new SimplePool();
    pool.publish(relayList, ev);            // feuern …

    // kurz warten, damit Verbindungen aufgehen und senden können
    await sleep(2500);

    pool.close(relayList);

    console.log(JSON.stringify({ id: ev.id, relays: relayList, published: true }, null, 2));
  } catch (e) {
    console.error(JSON.stringify({ error: e.message }, null, 2));
    process.exit(1);
  }
})();

Nutzung

Mit der .env-Datei musst du die Umgebungsvariablen nicht mehr in der Befehlszeile setzen:

# Einfache Nutzung mit .env-Datei
node post.js artikel-mapped.json

# Oder mit dem erweiterten Mapping direkt:
node adv-mapping.js artikel.md | node post.js /dev/stdin

Alternativ kannst du die Umgebungsvariablen auch weiterhin direkt setzen, falls nötig:

export NSEC="nsec1..."                  # oder: export NOSTR_SK="<hex>"
export RELAYS="wss://relay-rpi.edufeed.org/,wss://relay.damus.io,wss://relay.primal.net/,wss://nostr.win"
node post.js artikel-mapped.json

Vorteile des dotenv-Ansatzes:

  • Sicherere Speicherung von Schlüsseln (keine Befehlszeilenhistorie)
  • Konsistente Relay-Konfiguration zwischen Ausführungen
  • Einfacherer Workflow ohne Export-Befehle
  • Keine relayInit/Versionstricks nötig
  • SimplePool.publish() existiert in allen verbreiteten Builds
  • Wir warten 2.5 s, damit die Sockets senden; dann schließen wir sauber

Remote-Markdown-Dateien abrufen und posten

Mit dem fetch-and-post.js-Skript kannst du Markdown-Dateien direkt von einer URL abrufen, parsen und als Nostr-Event posten:

Installation der zusätzlichen Abhängigkeit

npm i axios

Nutzung des Remote-Fetching-Skripts

node fetch-and-post.js https://raw.githubusercontent.com/user/repo/main/artikel.md

Dieses Skript kombiniert alle vorherigen Funktionen:

  1. Abrufen der Markdown-Datei von einer URL
  2. Parsen des YAML-Frontmatters und Markdown-Inhalts
  3. Mapping zu einem Nostr-Kind-30023-Event
  4. Posten des Events an die konfigurierten Relays

Alle Konfigurationsparameter (nsec, Relays) werden aus der .env-Datei geladen.

Bulk-Verarbeitung von Git-Repositories

Mit dem bulk-fetch-post.js-Skript können mehrere Markdown-Dateien aus einem Git-Repository automatisch abgerufen, geparst und als Nostr-Events veröffentlicht werden.

Installation zusätzlicher Abhängigkeiten

npm i axios dotenv gray-matter nostr-tools ws commander

Konfiguration

  1. Passe die Repository-Informationen im Skript an:
// Konfiguration für die Forgejo/Gitea API
const repoOwner = 'Dein-Repository-Besitzer';
const repoName = 'Dein-Repository-Name';
const branch = 'main';
const path = 'Pfad/zu/markdown/dateien';
  1. Stelle sicher, dass deine .env-Datei konfiguriert ist:
NSEC=nsec1...
RELAYS=wss://relay1,wss://relay2
# Für private Repositories
API_TOKEN=dein_api_token

Ausführung

# Standardausführung
node bulk-fetch-post.js

# Mit Optionen
node bulk-fetch-post.js --dry-run --delay 10000 --verbose

Verfügbare Optionen

  • --dry-run: Führt das Skript aus, ohne Events zu posten (zum Testen)
  • --delay <ms>: Verzögerung zwischen Posts (Standard: 5000ms)
  • --verbose: Ausführlichere Ausgabe während der Ausführung

Anpassung für andere Git-Hosting-Plattformen

Das Skript ist für Forgejo/Gitea-APIs optimiert, kann aber für andere Plattformen angepasst werden. Siehe die Kommentare im Skript für GitLab- und GitHub-Beispiele.