// ======================================== // 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 { handleModularWorkflow } = 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émentation réelle avec AWS SDK try { const AWS = require('aws-sdk'); // Configure AWS SDK pour Digital Ocean Spaces const spacesEndpoint = new AWS.Endpoint('fra1.digitaloceanspaces.com'); const s3 = new AWS.S3({ endpoint: spacesEndpoint, accessKeyId: process.env.DO_ACCESS_KEY_ID || DO_CONFIG.accessKeyId, secretAccessKey: process.env.DO_SECRET_ACCESS_KEY || DO_CONFIG.secretAccessKey, region: 'fra1', s3ForcePathStyle: false, signatureVersion: 'v4' }); const uploadParams = { Bucket: 'autocollant', Key: path.startsWith('/') ? path.substring(1) : path, Body: html, ContentType: 'text/html', ACL: 'public-read' }; logSh(`🚀 Uploading to DO Spaces: ${uploadParams.Key}`, 'INFO'); const result = await s3.upload(uploadParams).promise(); logSh(`✅ Upload successful: ${result.Location}`, 'INFO'); return { ok: true, location: result.Location, etag: result.ETag, bucket: result.Bucket, key: result.Key, path, length: html.length }; } catch (error) { logSh(`❌ DO Spaces upload failed: ${error.message}`, 'ERROR'); throw error; } } 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 handleModularWorkflow(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'); } // Try direct access first (new approach) const directPath = fileName; const directUrl = `https://autocollant.fra1.digitaloceanspaces.com/${directPath}`; logSh(`🌊 Récupération XML: ${fileName} (direct access)`, 'DEBUG'); logSh(`🔗 URL directe: ${directUrl}`, 'DEBUG'); try { // Try direct public access first (faster) const directResponse = await axios.get(directUrl, { timeout: 10000 }); if (directResponse.status === 200 && directResponse.data.includes(' `${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 = { deployArticle, triggerAutonomousWorkflow, runAutonomousWorkflowFromTrigger, fetchXMLFromDigitalOcean, fetchXMLFromDigitalOceanSimple, readCSVDataWithXMLFileName, markRowAsProcessed, markRowAsError, testDigitalOceanConnection, setupAutonomousTrigger, DO_CONFIG };