import express from 'express'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import yaml from 'js-yaml'; import Emusks from './emusks-local/index.js'; // --- GESTION DES CHEMINS (ES Modules) --- const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // --- LECTURE DE LA CONFIGURATION (YAML ou JSON) --- let config = {}; const configPaths = [ path.join(__dirname, 'config.yml'), path.join(__dirname, 'config.yaml'), path.join(__dirname, '../config.yml'), path.join(process.cwd(), 'config.yml') ]; for (const configPath of configPaths) { try { if (fs.existsSync(configPath)) { const fileContents = fs.readFileSync(configPath, 'utf8'); config = yaml.load(fileContents); console.log(`✅ Configuration chargée depuis : ${configPath}`); break; } } catch (e) { console.warn(`⚠️ Erreur lecture config ${configPath}: ${e.message}`); } } if (Object.keys(config).length === 0) { console.warn("⚠️ Aucun fichier de configuration trouvé, utilisation des variables d'environnement."); } // --- CONFIGURATION --- const API_SECRET = config.api_secret || process.env.API_SECRET || 'secret_par_defaut'; const PORT = config.port || process.env.PORT || 3000; const NODE_ENV = process.env.NODE_ENV || 'development'; console.log(`🔧 Environment: ${NODE_ENV}`); console.log(`🔐 API Secret configured: ${API_SECRET ? 'Yes' : 'No'}`); console.log(`🚀 Port: ${PORT}`); // --- INITIALISATION EXPRESS --- const app = express(); // Middleware pour parser le JSON (avec limite augmentée si besoin) app.use(express.json({ limit: '10mb' })); // Logging des requêtes (seulement en dev) if (NODE_ENV === 'development') { app.use((req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`); next(); }); } // --- ENDPOINT DE SANTÉ (Health Check) --- app.get('/health', (req, res) => { res.json({ status: 'ok', service: 'twitter-worker', timestamp: new Date().toISOString(), environment: NODE_ENV }); }); // --- ENDPOINT RACINE --- app.get('/', (req, res) => { res.json({ service: 'Twitter Worker API', version: '1.0.0', endpoints: { health: 'GET /health', call: 'POST /api/twitter/call' } }); }); // --- MIDDLEWARE DE SÉCURITÉ --- const authMiddleware = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).json({ error: 'Non autorisé. Header Authorization manquant.', required: 'Bearer ' }); } const [scheme, token] = authHeader.split(' '); if (scheme !== 'Bearer' || token !== API_SECRET) { console.warn(`⚠️ Tentative d'accès non autorisé depuis ${req.ip}`); return res.status(401).json({ error: 'Non autorisé. Clé API invalide.' }); } next(); }; // Appliquer l'authentification uniquement sur les routes API app.use('/api', authMiddleware); // --- ENDPOINT PROXY DYNAMIQUE --- app.post('/api/twitter/call', async (req, res) => { const { auth_token, proxy, domain, method, args } = req.body; // Validation des paramètres requis if (!auth_token) { return res.status(400).json({ error: "auth_token requis.", hint: "Le token d'authentification Twitter est obligatoire." }); } if (!domain || !method) { return res.status(400).json({ error: "domain et method requis.", hint: "Exemple: domain='twitter', method='search'" }); } console.log(`📡 Requête reçue: ${domain}.${method}()`); let client = null; try { // Initialisation du client Emusks client = new Emusks(); // Connexion avec les paramètres fournis await client.login({ auth_token, proxy, client: "web" }); // Vérification que la méthode existe if (!client[domain] || typeof client[domain][method] !== 'function') { return res.status(400).json({ error: `La méthode client.${domain}.${method}() n'existe pas.`, available_domains: Object.keys(client).filter(k => typeof client[k] === 'object') }); } // Exécution de la méthode const startTime = Date.now(); const result = await client[domain][method](...(args || [])); const duration = Date.now() - startTime; console.log(`✅ Succès: ${domain}.${method}() en ${duration}ms`); res.json({ success: true, data: result, meta: { duration_ms: duration, timestamp: new Date().toISOString() } }); } catch (error) { const duration = Date.now() - (startTime || Date.now()); console.error(`❌ [ERREUR] Action: ${domain}.${method}`); console.error(` Message: ${error.message}`); console.error(` Duration: ${duration}ms`); if (NODE_ENV === 'development') { console.error(error.stack); } res.status(500).json({ success: false, error: error.message || "Erreur interne du serveur", details: error.response?.data || error.response?.body || null, meta: { duration_ms: duration, timestamp: new Date().toISOString() } }); } finally { // Nettoyage si nécessaire if (client && typeof client.logout === 'function') { // optionnel: await client.logout(); } } }); // --- GESTION DES ERREURS 404 --- app.use((req, res) => { res.status(404).json({ error: 'Endpoint non trouvé', path: req.path, method: req.method, available_endpoints: ['GET /', 'GET /health', 'POST /api/twitter/call'] }); }); // --- GESTION DES ERREURS GLOBALES --- app.use((err, req, res, next) => { console.error('🔥 Erreur globale:', err.message); res.status(500).json({ success: false, error: 'Erreur interne du serveur', message: NODE_ENV === 'development' ? err.message : undefined }); }); // --- DÉMARRAGE DU SERVEUR --- const server = app.listen(PORT, () => { console.log(''); console.log('╔════════════════════════════════════════════╗'); console.log('║ 🐦 Twitter Worker API Démarrée ║'); console.log('╠════════════════════════════════════════════╣'); console.log(`║ 🌍 URL: http://localhost:${PORT} ║`); console.log(`║ 🔐 Auth: Bearer ${API_SECRET.substring(0, 8)}... ║`); console.log(`║ 🚀 Env: ${NODE_ENV.padEnd(28)}║`); console.log('╚════════════════════════════════════════════╝'); console.log(''); }); // --- GESTION DES SIGNAUX POUR ARRÊT PROPRE --- process.on('SIGTERM', () => { console.log('📶 Signal SIGTERM reçu, fermeture en cours...'); server.close(() => { console.log('✅ Serveur fermé proprement'); process.exit(0); }); }); process.on('SIGINT', () => { console.log('📶 Signal SIGINT reçu (Ctrl+C), fermeture en cours...'); server.close(() => { console.log('✅ Serveur fermé proprement'); process.exit(0); }); }); // Gestion des erreurs non capturées process.on('uncaughtException', (err) => { console.error('💥 Exception non capturée:', err); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('💥 Rejection non gérée:', reason); process.exit(1); });