#!/usr/bin/env node /** * Met à jour la date de publication des posts Lemmy à partir des articles Hugo. * Pré-requis : accès en écriture à la base Postgres de Lemmy * (par exemple via LEMMY_DATABASE_URL=postgres:///lemmy?host=/run/postgresql&user=lemmy * et exécution en tant qu'utilisateur système lemmy). * * Règles : * - L'article doit contenir un frontmatter valide avec un champ date. * - L'article doit contenir un comments_url pointant vers /post/{id}. * - La date est appliquée sur post.published (timestamp avec fuseau Hugo). */ const path = require("node:path"); const { Pool } = require("pg"); const { collectBundles } = require("./lib/content"); const { parseFrontmatterDate } = require("./lib/datetime"); const { readFrontmatterFile } = require("./lib/frontmatter"); const { loadEnv } = require("./lib/env"); const CONTENT_ROOT = path.join(__dirname, "..", "content"); const DEFAULT_DATABASE_URL = "postgres:///lemmy?host=/run/postgresql&user=lemmy"; main().then( () => { process.exit(0); }, (error) => { console.error(`❌ Mise à jour interrompue : ${error.message}`); process.exit(1); } ); /** * Point d'entrée : collecte les articles, se connecte à Postgres, applique les dates. */ async function main() { loadEnv(); const databaseUrl = resolveDatabaseUrl(); const pool = new Pool({ connectionString: databaseUrl }); const bundles = await collectBundles(CONTENT_ROOT); const articles = collectArticlesWithPostId(bundles); if (articles.length === 0) { console.log("Aucun article muni d'un comments_url et d'une date valide."); await pool.end(); return; } let updated = 0; let unchanged = 0; let missing = 0; for (const article of articles) { const targetDate = article.publication.set({ millisecond: 0 }); const iso = targetDate.toISO(); const row = await fetchPost(pool, article.postId); if (!row) { missing += 1; console.warn(`⚠️ Post ${article.postId} introuvable pour ${article.bundle.relativePath}`); continue; } const currentIso = new Date(row.published).toISOString(); if (currentIso === targetDate.toUTC().toISO()) { unchanged += 1; continue; } await applyDate(pool, article.postId, iso); updated += 1; console.log(`✅ Post ${article.postId} mis à ${iso} (${article.bundle.relativePath})`); } await pool.end(); console.log(""); console.log("Résumé des ajustements Lemmy"); console.log(`Posts mis à jour : ${updated}`); console.log(`Posts déjà alignés : ${unchanged}`); console.log(`Posts introuvables : ${missing}`); } /** * Détermine l'URL de connexion Postgres. * @returns {string} Chaîne de connexion. */ function resolveDatabaseUrl() { if (typeof process.env.LEMMY_DATABASE_URL === "string" && process.env.LEMMY_DATABASE_URL.trim()) { return process.env.LEMMY_DATABASE_URL.trim(); } if (typeof process.env.DATABASE_URL === "string" && process.env.DATABASE_URL.trim()) { return process.env.DATABASE_URL.trim(); } return DEFAULT_DATABASE_URL; } /** * Construit la liste des articles éligibles avec identifiant de post Lemmy. * @param {Array} bundles Bundles Hugo. * @returns {Array} Articles prêts à être appliqués. */ function collectArticlesWithPostId(bundles) { const articles = []; for (const bundle of bundles) { const frontmatter = readFrontmatterFile(bundle.indexPath); if (!frontmatter) { continue; } const publication = parseFrontmatterDate(frontmatter.data?.date); if (!publication) { continue; } const title = typeof frontmatter.data?.title === "string" ? frontmatter.data.title.trim() : ""; if (!title) { continue; } const commentsUrl = typeof frontmatter.data?.comments_url === "string" ? frontmatter.data.comments_url.trim() : ""; if (!commentsUrl) { continue; } const postId = extractPostId(commentsUrl); if (postId === null) { continue; } articles.push({ bundle, publication, postId, }); } articles.sort((a, b) => { const diff = a.publication.toMillis() - b.publication.toMillis(); if (diff !== 0) { return diff; } return a.bundle.relativePath.localeCompare(b.bundle.relativePath); }); return articles; } /** * Extrait l'identifiant numérique d'un comments_url Lemmy. * @param {string} url URL issue du frontmatter. * @returns {number|null} Identifiant ou null si non reconnu. */ function extractPostId(url) { const trimmed = url.trim(); if (!trimmed) { return null; } const normalized = trimmed.replace(/\/+$/, ""); const match = normalized.match(/\/(?:post|c\/[^/]+\/post)\/(\d+)(?:$|\?)/i); if (!match) { return null; } return Number.parseInt(match[1], 10); } /** * Récupère un post Lemmy par identifiant. * @param {Pool} pool Pool Postgres. * @param {number} postId Identifiant du post. * @returns {Promise} Enregistrement ou null. */ async function fetchPost(pool, postId) { const result = await pool.query("select id, published from post where id = $1", [postId]); if (result.rowCount !== 1) { return null; } return result.rows[0]; } /** * Applique la date ISO fournie sur le post ciblé. * @param {Pool} pool Pool Postgres. * @param {number} postId Identifiant du post. * @param {string} isoDate Timestamp ISO. */ async function applyDate(pool, postId, isoDate) { await pool.query("update post set published = $1 where id = $2", [isoDate, postId]); }