diff --git a/tools/post-devlog.mjs b/tools/post-devlog.mjs new file mode 100644 index 0000000..394d587 --- /dev/null +++ b/tools/post-devlog.mjs @@ -0,0 +1,133 @@ +#!/usr/bin/env node +// Devlog aus DIESEM Repo posten. +// +// tools/devlog.py im EcoGame-Repo geht dafür nicht: es zählt EcoGame-Commits, +// bricht ohne solche ab, betitelt den Post als „Devlog — EcoGame" und löscht +// die Bilder nach dem Senden. Dieses Skript zählt die Commits von d4rkbot, +// lässt die Bilder liegen und kann vorher trocken laufen. +// +// Aufruf: +// node tools/post-devlog.mjs --text docs/devlog-hub.md --images A:/eco/devlog-hub-bilder +// node tools/post-devlog.mjs --text docs/devlog-hub.md --dry +// +// Die Webhook-Adresse (enthält das Secret) kommt aus der Umgebungsvariable +// DEVLOG_WEBHOOK oder aus einer Datei — Standard ist die vorhandene +// A:/eco/EcoGame/tools/.devlog_webhook. Sie wird nie ausgegeben. +import { execFileSync } from 'node:child_process'; +import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'; +import { basename, dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const MAX_IMAGES = 4; +const IMAGE_PATTERN = /\.(png|jpe?g|gif|webp)$/i; + +function arg(name, fallback = null) { + const i = process.argv.indexOf(`--${name}`); + return i !== -1 && process.argv[i + 1] && !process.argv[i + 1].startsWith('--') + ? process.argv[i + 1] + : fallback; +} +const has = (name) => process.argv.includes(`--${name}`); + +function webhookUrl() { + const fromEnv = String(process.env.DEVLOG_WEBHOOK ?? '').trim(); + if (fromEnv) return fromEnv; + const file = arg('webhook-file', 'A:/eco/EcoGame/tools/.devlog_webhook'); + if (!existsSync(file)) { + console.error( + `Keine Webhook-Adresse gefunden.\n` + + ` DEVLOG_WEBHOOK setzen oder --webhook-file angeben.\n` + + ` Gesucht wurde: ${file}` + ); + process.exit(1); + } + return readFileSync(file, 'utf8').trim(); +} + +/** Commits dieses Repos seit — nur für die Fußzeile */ +function commitCount(since) { + try { + const out = execFileSync('git', ['-C', REPO, 'log', `--since=${since}`, '--oneline', '--no-merges'], { + encoding: 'utf8', + }); + return out.split('\n').filter((l) => l.trim()).length; + } catch { + return 0; + } +} + +function collectImages(dir) { + if (!dir) return []; + if (!existsSync(dir) || !statSync(dir).isDirectory()) { + console.error(`Bilder-Ordner nicht gefunden: ${dir}`); + process.exit(1); + } + const files = readdirSync(dir).filter((f) => IMAGE_PATTERN.test(f)).sort(); + if (files.length > MAX_IMAGES) { + console.warn(`Hinweis: ${files.length} Bilder gefunden, es werden die ersten ${MAX_IMAGES} gepostet.`); + } + return files.slice(0, MAX_IMAGES).map((f) => join(dir, f)); +} + +const textFile = arg('text'); +if (!textFile) { + console.error('Bitte --text angeben (Markdown mit der Prosa).'); + process.exit(1); +} +if (!existsSync(textFile)) { + console.error(`Textdatei nicht gefunden: ${textFile}`); + process.exit(1); +} + +const prose = readFileSync(textFile, 'utf8').trim(); +if (!prose) { + console.error('Die Textdatei ist leer — nicht gepostet.'); + process.exit(1); +} + +const project = arg('project', 'd4rkbot'); +const since = arg('since', 'yesterday'); +const images = collectImages(arg('images')); +const n = commitCount(since); + +const description = `${prose}\n\n*${n} Commit${n === 1 ? '' : 's'} heute — Details im [Repo](https://git.d4rkst3r.de/D4rkst3r/${project})*`; +const payload = { + embeds: [{ + title: `Devlog — ${project}`, + description, + url: `https://git.d4rkst3r.de/D4rkst3r/${project}`, + }], +}; + +if (description.length > 3900) { + console.warn(`Achtung: Text ist ${description.length} Zeichen — Discord kürzt ab 4096.`); +} + +console.log(`Projekt: ${project}`); +console.log(`Text: ${textFile} (${prose.length} Zeichen)`); +console.log(`Commits: ${n} seit ${since}`); +console.log(`Bilder: ${images.length ? images.map((i) => basename(i)).join(', ') : '— keine —'}`); + +if (has('dry')) { + console.log('\n--- Vorschau (nichts gesendet) ---\n'); + console.log(description); + process.exit(0); +} + +const form = new FormData(); +// Als Textfeld anhängen, nicht als Blob: ein Blob käme als Datei-Teil an und +// würde vom Endpoint zusammen mit den Nicht-Bildern verworfen. +form.append('payload_json', JSON.stringify(payload)); +for (const [i, file] of images.entries()) { + form.append(`files[${i}]`, new Blob([readFileSync(file)]), basename(file)); +} + +const res = await fetch(webhookUrl(), { method: 'POST', body: form }); +if (!res.ok) { + // Die Adresse enthält das Secret — deshalb nur Status und Antworttext zeigen + console.error(`\nFehlgeschlagen: HTTP ${res.status}`); + console.error(await res.text().catch(() => '')); + process.exit(1); +} +console.log(`\nGepostet (HTTP ${res.status}). Die Bilder bleiben liegen.`);