// ======================================== // FICHIER: DigitalOceanWorkflow.js - REFACTORISÉ POUR NODE.JS // RESPONSABILITÉ: Orchestration + Interface Digital Ocean UNIQUEMENT // ======================================== const crypto = require('crypto'); const axios = require('axios'); const { GoogleSpreadsheet } = require('google-spreadsheet'); const { JWT } = require('google-auth-library'); // Import des autres modules du projet (à adapter selon votre structure) const { logSh } = require('./ErrorReporting'); const { handleFullWorkflow } = require('./Main'); const { getPersonalities, selectPersonalityWithAI } = require('./BrainConfig'); // ============= CONFIGURATION DIGITAL OCEAN ============= const DO_CONFIG = { endpoint: 'https://autocollant.fra1.digitaloceanspaces.com', bucketName: 'autocollant', accessKeyId: 'DO801XTYPE968NZGAQM3', secretAccessKey: '5aCCBiS9K+J8gsAe3M3/0GlliHCNjtLntwla1itCN1s', region: 'fra1' }; // Configuration Google Sheets const SHEET_CONFIG = { sheetId: '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c', serviceAccountEmail: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL, privateKey: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'), // Alternative: utiliser fichier JSON directement keyFile: './seo-generator-470715-85d4a971c1af.json' }; async function deployArticle({ path, html, dryRun = false, ...rest }) { if (!path || typeof html !== 'string') { const err = new Error('deployArticle: invalid payload (requires {path, html})'); err.code = 'E_PAYLOAD'; throw err; } if (dryRun) { return { ok: true, dryRun: true, length: html.length, path, meta: rest || {} }; } // --- Impl réelle à toi ici (upload DO Spaces / API / SSH etc.) --- // return await realDeploy({ path, html, ...rest }); // Placeholder pour ne pas casser l'appel si pas encore implémenté return { ok: true, dryRun: false, path, length: html.length }; } module.exports.deployArticle = module.exports.deployArticle || deployArticle; // ============= TRIGGER PRINCIPAL REMPLACÉ PAR WEBHOOK/API ============= /** * Point d'entrée pour déclencher le workflow * Remplace le trigger onEdit d'Apps Script * @param {number} rowNumber - Numéro de ligne à traiter * @returns {Promise} - Résultat du workflow */ async function triggerAutonomousWorkflow(rowNumber) { try { logSh('🚀 TRIGGER AUTONOME DÉCLENCHÉ (Digital Ocean)', 'INFO'); // Anti-bouncing simulé await new Promise(resolve => setTimeout(resolve, 2000)); return await runAutonomousWorkflowFromTrigger(rowNumber); } catch (error) { logSh(`❌ Erreur trigger autonome DO: ${error.toString()}`, 'ERROR'); throw error; } } /** * ORCHESTRATEUR: Prépare les données et délègue à Main.js */ async function runAutonomousWorkflowFromTrigger(rowNumber) { const startTime = Date.now(); try { logSh(`🎬 ORCHESTRATION AUTONOME - LIGNE ${rowNumber}`, 'INFO'); // 1. LIRE DONNÉES CSV + XML FILENAME const csvData = await readCSVDataWithXMLFileName(rowNumber); logSh(`✅ CSV: ${csvData.mc0}, XML: ${csvData.xmlFileName}`, 'INFO'); // 2. RÉCUPÉRER XML DEPUIS DIGITAL OCEAN const xmlTemplate = await fetchXMLFromDigitalOceanSimple(csvData.xmlFileName); logSh(`✅ XML récupéré: ${xmlTemplate.length} caractères`, 'INFO'); // 3. 🎯 DÉLÉGUER LE WORKFLOW À MAIN.JS const workflowData = { rowNumber: rowNumber, xmlTemplate: Buffer.from(xmlTemplate).toString('base64'), // Encoder comme Make.com csvData: csvData, source: 'digital_ocean_autonomous' }; const result = await handleFullWorkflow(workflowData); const duration = Date.now() - startTime; logSh(`🏆 ORCHESTRATION TERMINÉE en ${Math.round(duration/1000)}s`, 'INFO'); // 4. MARQUER LIGNE COMME TRAITÉE await markRowAsProcessed(rowNumber, result); return result; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ ERREUR ORCHESTRATION: ${error.toString()}`, 'ERROR'); await markRowAsError(rowNumber, error.toString()); throw error; } } // ============= INTERFACE DIGITAL OCEAN ============= async function fetchXMLFromDigitalOceanSimple(fileName) { const filePath = `wp-content/XML/${fileName}`; const fileUrl = `${DO_CONFIG.endpoint}/${filePath}`; try { const response = await axios.get(fileUrl); // Sans auth return response.data; } catch (error) { throw new Error(`Fichier non accessible: ${error.message}`); } } /** * Récupérer XML depuis Digital Ocean Spaces avec authentification */ async function fetchXMLFromDigitalOcean(fileName) { if (!fileName) { throw new Error('Nom de fichier XML requis'); } const filePath = `wp-content/XML/${fileName}`; logSh(`🌊 Récupération XML: ${fileName} , ${filePath}`, 'DEBUG'); const fileUrl = `${DO_CONFIG.endpoint}/${filePath}`; logSh(`🔗 URL complète: ${fileUrl}`, 'DEBUG'); const signature = generateAWSSignature(filePath); try { const response = await axios.get(fileUrl, { headers: signature.headers }); logSh(`📡 Response code: ${response.status}`, 'DEBUG'); logSh(`📄 Response: ${response.data.toString()}`, 'DEBUG'); if (response.status === 200) { return response.data; } else { throw new Error(`HTTP ${response.status}: ${response.data}`); } } catch (error) { logSh(`❌ Erreur DO complète: ${error.toString()}`, 'ERROR'); throw error; } } /** * Lire données CSV avec nom fichier XML (colonne J) */ async function readCSVDataWithXMLFileName(rowNumber) { try { // Configuration Google Sheets - avec fallback sur fichier JSON let serviceAccountAuth; if (SHEET_CONFIG.serviceAccountEmail && SHEET_CONFIG.privateKey) { // Utiliser variables d'environnement serviceAccountAuth = new JWT({ email: SHEET_CONFIG.serviceAccountEmail, key: SHEET_CONFIG.privateKey, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } else { // Utiliser fichier JSON serviceAccountAuth = new JWT({ keyFile: SHEET_CONFIG.keyFile, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } const doc = new GoogleSpreadsheet(SHEET_CONFIG.sheetId, serviceAccountAuth); await doc.loadInfo(); const sheet = doc.sheetsByTitle['instructions']; if (!sheet) { throw new Error('Sheet "instructions" non trouvée'); } await sheet.loadCells(`A${rowNumber}:I${rowNumber}`); const slug = sheet.getCell(rowNumber - 1, 0).value; const t0 = sheet.getCell(rowNumber - 1, 1).value; const mc0 = sheet.getCell(rowNumber - 1, 2).value; const tMinus1 = sheet.getCell(rowNumber - 1, 3).value; const lMinus1 = sheet.getCell(rowNumber - 1, 4).value; const mcPlus1 = sheet.getCell(rowNumber - 1, 5).value; const tPlus1 = sheet.getCell(rowNumber - 1, 6).value; const lPlus1 = sheet.getCell(rowNumber - 1, 7).value; const xmlFileName = sheet.getCell(rowNumber - 1, 8).value; if (!xmlFileName || xmlFileName.toString().trim() === '') { throw new Error(`Nom fichier XML manquant colonne I, ligne ${rowNumber}`); } let cleanFileName = xmlFileName.toString().trim(); if (!cleanFileName.endsWith('.xml')) { cleanFileName += '.xml'; } // Récupérer personnalité (délègue au système existant BrainConfig.js) const personalities = await getPersonalities(); // Pas de paramètre, lit depuis JSON const selectedPersonality = await selectPersonalityWithAI(mc0, t0, personalities); return { rowNumber: rowNumber, slug: slug, t0: t0, mc0: mc0, tMinus1: tMinus1, lMinus1: lMinus1, mcPlus1: mcPlus1, tPlus1: tPlus1, lPlus1: lPlus1, xmlFileName: cleanFileName, personality: selectedPersonality }; } catch (error) { logSh(`❌ Erreur lecture CSV: ${error.toString()}`, 'ERROR'); throw error; } } // ============= STATUTS ET VALIDATION ============= /** * Vérifier si le workflow doit être déclenché * En Node.js, cette logique sera adaptée selon votre stratégie (webhook, polling, etc.) */ function shouldTriggerWorkflow(rowNumber, xmlFileName) { if (!rowNumber || rowNumber <= 1) { return false; } if (!xmlFileName || xmlFileName.toString().trim() === '') { logSh('⚠️ Pas de fichier XML (colonne J), workflow ignoré', 'WARNING'); return false; } return true; } async function markRowAsProcessed(rowNumber, result) { try { // Configuration Google Sheets - avec fallback sur fichier JSON let serviceAccountAuth; if (SHEET_CONFIG.serviceAccountEmail && SHEET_CONFIG.privateKey) { serviceAccountAuth = new JWT({ email: SHEET_CONFIG.serviceAccountEmail, key: SHEET_CONFIG.privateKey, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } else { serviceAccountAuth = new JWT({ keyFile: SHEET_CONFIG.keyFile, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } const doc = new GoogleSpreadsheet(SHEET_CONFIG.sheetId, serviceAccountAuth); await doc.loadInfo(); const sheet = doc.sheetsByTitle['instructions']; // Vérifier et ajouter headers si nécessaire await sheet.loadCells('K1:N1'); if (!sheet.getCell(0, 10).value) { sheet.getCell(0, 10).value = 'Status'; sheet.getCell(0, 11).value = 'Processed_At'; sheet.getCell(0, 12).value = 'Article_ID'; sheet.getCell(0, 13).value = 'Source'; await sheet.saveUpdatedCells(); } // Marquer la ligne await sheet.loadCells(`K${rowNumber}:N${rowNumber}`); sheet.getCell(rowNumber - 1, 10).value = '✅ DO_SUCCESS'; sheet.getCell(rowNumber - 1, 11).value = new Date().toISOString(); sheet.getCell(rowNumber - 1, 12).value = result.articleStorage?.articleId || ''; sheet.getCell(rowNumber - 1, 13).value = 'Digital Ocean'; await sheet.saveUpdatedCells(); logSh(`✅ Ligne ${rowNumber} marquée comme traitée`, 'INFO'); } catch (error) { logSh(`⚠️ Erreur marquage statut: ${error.toString()}`, 'WARNING'); } } async function markRowAsError(rowNumber, errorMessage) { try { // Configuration Google Sheets - avec fallback sur fichier JSON let serviceAccountAuth; if (SHEET_CONFIG.serviceAccountEmail && SHEET_CONFIG.privateKey) { serviceAccountAuth = new JWT({ email: SHEET_CONFIG.serviceAccountEmail, key: SHEET_CONFIG.privateKey, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } else { serviceAccountAuth = new JWT({ keyFile: SHEET_CONFIG.keyFile, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); } const doc = new GoogleSpreadsheet(SHEET_CONFIG.sheetId, serviceAccountAuth); await doc.loadInfo(); const sheet = doc.sheetsByTitle['instructions']; await sheet.loadCells(`K${rowNumber}:N${rowNumber}`); sheet.getCell(rowNumber - 1, 10).value = '❌ DO_ERROR'; sheet.getCell(rowNumber - 1, 11).value = new Date().toISOString(); sheet.getCell(rowNumber - 1, 12).value = errorMessage.substring(0, 100); sheet.getCell(rowNumber - 1, 13).value = 'DO Error'; await sheet.saveUpdatedCells(); } catch (error) { logSh(`⚠️ Erreur marquage erreur: ${error.toString()}`, 'WARNING'); } } // ============= SIGNATURE AWS V4 ============= function generateAWSSignature(filePath) { const now = new Date(); const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, ''); const timeStamp = now.toISOString().replace(/[-:]/g, '').slice(0, -5) + 'Z'; const headers = { 'Host': DO_CONFIG.endpoint.replace('https://', ''), 'X-Amz-Date': timeStamp, 'X-Amz-Content-Sha256': 'UNSIGNED-PAYLOAD' }; const credentialScope = `${dateStamp}/${DO_CONFIG.region}/s3/aws4_request`; const canonicalHeaders = Object.keys(headers) .sort() .map(key => `${key.toLowerCase()}:${headers[key]}`) .join('\n'); const signedHeaders = Object.keys(headers) .map(key => key.toLowerCase()) .sort() .join(';'); const canonicalRequest = [ 'GET', `/${filePath}`, '', canonicalHeaders + '\n', signedHeaders, 'UNSIGNED-PAYLOAD' ].join('\n'); const stringToSign = [ 'AWS4-HMAC-SHA256', timeStamp, credentialScope, crypto.createHash('sha256').update(canonicalRequest).digest('hex') ].join('\n'); // Calculs HMAC étape par étape const kDate = crypto.createHmac('sha256', 'AWS4' + DO_CONFIG.secretAccessKey).update(dateStamp).digest(); const kRegion = crypto.createHmac('sha256', kDate).update(DO_CONFIG.region).digest(); const kService = crypto.createHmac('sha256', kRegion).update('s3').digest(); const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest(); const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex'); headers['Authorization'] = `AWS4-HMAC-SHA256 Credential=${DO_CONFIG.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`; return { headers: headers }; } // ============= SETUP ET TEST ============= /** * Configuration du trigger autonome - Remplacé par webhook ou polling en Node.js */ function setupAutonomousTrigger() { logSh('⚙️ Configuration trigger autonome Digital Ocean...', 'INFO'); // En Node.js, vous pourriez utiliser: // - Express.js avec webhooks // - Cron jobs avec node-cron // - Polling de la Google Sheet // - WebSocket connections logSh('✅ Configuration prête pour webhooks/polling Node.js', 'INFO'); logSh('🎯 Mode: Webhook/API → Digital Ocean → Main.js', 'INFO'); } async function testDigitalOceanConnection() { logSh('🧪 Test connexion Digital Ocean...', 'INFO'); try { const testFiles = ['template1.xml', 'plaque-rue.xml', 'test.xml']; for (const fileName of testFiles) { try { const content = await fetchXMLFromDigitalOceanSimple(fileName); logSh(`✅ Fichier '${fileName}' accessible (${content.length} chars)`, 'INFO'); return true; } catch (error) { logSh(`⚠️ '${fileName}' non accessible: ${error.toString()}`, 'DEBUG'); } } logSh('❌ Aucun fichier test accessible dans DO', 'ERROR'); return false; } catch (error) { logSh(`❌ Test DO échoué: ${error.toString()}`, 'ERROR'); return false; } } // ============= EXPORTS ============= module.exports = { triggerAutonomousWorkflow, runAutonomousWorkflowFromTrigger, fetchXMLFromDigitalOcean, fetchXMLFromDigitalOceanSimple, readCSVDataWithXMLFileName, markRowAsProcessed, markRowAsError, testDigitalOceanConnection, setupAutonomousTrigger, DO_CONFIG };