const { Client, LocalAuth } = require('whatsapp-web.js'); const express = require('express'); const qrcode = require('qrcode'); const fs = require('fs-extra'); const path = require('path'); const app = express(); app.use(express.json()); app.use(express.static(path.join(__dirname, 'ui'))); let lastQr = ""; let isReady = false; let isInitializing = true; const SESSION_DATA = '/data/auth'; // --- AUTOMATISCHE SELBSTREINIGUNG BEIM START --- // Löscht alte Chromium-Sperrdateien, die das "LADE CLIENT..."-Hängen verursachen try { const lockFolder = path.join(SESSION_DATA, 'session-ha_bridge_session'); const lockFile = path.join(lockFolder, 'SingletonLock'); if (fs.existsSync(lockFile)) { fs.removeSync(lockFile); console.log('🔒 Blockierendes Chromium-SingletonLock wurde automatisch entfernt!'); } } catch (e) { console.log('Info: Keine blockierende Sperrdatei beim Start gefunden.'); } async function handleCrash(errorContext) { console.error(`💥 CRITICAL ERROR CAUGHT: ${errorContext}`); isReady = false; try { console.log('Versuche Browser sauber zu schließen...'); if (client && client.pupBrowser) { await client.pupBrowser.close().catch(() => {}); } } catch (e) { console.error('Browser-Close fehlgeschlagen:', e); } console.log('Add-on wird beendet. HA Watchdog startet es neu...'); process.exit(1); } process.on('uncaughtException', async (err) => { if (err.message.includes('ProtocolError') || err.message.includes('Runtime.callFunctionOn') || err.message.includes('Target closed')) { await handleCrash('Puppeteer Protocol Timeout / Crash'); } else { console.error('Uncaught Exception:', err); } }); process.on('unhandledRejection', async (reason, promise) => { if (reason && reason.message && reason.message.includes('Session closed')) { await handleCrash('Unhandled Puppeteer Session Rejection'); } else { console.error('Unhandled Rejection at:', promise, 'reason:', reason); } }); const client = new Client({ authStrategy: new LocalAuth({ dataPath: SESSION_DATA, clientId: "ha_bridge_session" }), authTimeoutMs: 90000, qrMaxRetries: 5, puppeteer: { executablePath: '/usr/bin/chromium-browser', protocolTimeout: 0, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-extensions' // '--single-process' und '--no-zygote' wurden entfernt, da sie in Alpine 3.19 Hangs verursachen können! ] } }); client.on('qr', (qr) => { lastQr = qr; isReady = false; isInitializing = false; console.log('Neuer QR Code generiert.'); }); client.on('ready', () => { isReady = true; lastQr = ""; isInitializing = false; console.log('WhatsApp ist bereit!'); }); client.on('disconnected', async (reason) => { console.log(`WhatsApp Verbindung getrennt (Grund: ${reason}).`); await handleCrash('Externer Disconnect'); }); // --- ROUTES --- app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'ui', 'index.html')); }); app.get('/api/status', async (req, res) => { try { let qrData = null; if (lastQr) { qrData = await qrcode.toDataURL(lastQr); } let clientState = 'DISCONNECTED'; if (isReady) { clientState = await client.getState().catch(() => 'ERROR'); } res.json({ isReady: isReady, hasQr: lastQr !== "", qrCode: qrData, isInitializing: isInitializing, clientState: clientState }); } catch (e) { res.status(500).json({ error: e.message }); } }); // NOTFALL-SICHER: Löscht die Session radikal, selbst wenn der Client eingefroren ist! app.post('/auth/logout', async (req, res) => { try { console.log("Logout erzwungen über Web-UI..."); if (client) { await client.logout().catch(() => {}); await client.destroy().catch(() => {}); } await fs.remove(SESSION_DATA).catch(() => {}); res.json({ success: true, message: "Session radikal gelöscht. Addon startet jetzt sauber neu..." }); setTimeout(() => process.exit(0), 1500); } catch (err) { res.status(500).json({ error: err.message }); } }); // NOTFALL-RESET: Putzt das komplette Verzeichnis leer app.delete('/system/reset', async (req, res) => { try { console.log("Factory Reset erzwungen..."); if (client) await client.destroy().catch(() => {}); await fs.emptyDir('/data').catch(() => {}); res.json({ success: true, message: "Werkseinstellungen erzwungen. Alles gelöscht. Neustart..." }); setTimeout(() => process.exit(0), 1500); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/send', async (req, res) => { const { number, message } = req.body; if (!isReady) return res.status(503).json({ error: 'Client nicht bereit' }); try { const chatId = number.includes('@c.us') ? number : `${number}@c.us`; await client.sendMessage(chatId, message); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); console.log('Initialisiere WhatsApp Client...'); client.initialize().catch(async (err) => { await handleCrash(`Initialisierungsfehler: ${err.message}`); }); app.listen(3000, () => console.log('WhatsApp Bridge Server läuft auf Port 3000'));