Stabilisiere WhatsApp-Verbindung
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
# CHANGELOG - Whatsapp Bridge Addon
|
# CHANGELOG - Whatsapp Bridge Addon
|
||||||
|
|
||||||
|
## Release 1.4.0 - 2026-08-04
|
||||||
|
- Fixed the QR retry restart loop; QR codes now remain available until scanned.
|
||||||
|
- Added automatic client recovery with bounded exponential backoff.
|
||||||
|
- Added a connection health check that recovers silent Chromium/network hangs.
|
||||||
|
- Updated whatsapp-web.js and the Alpine/Chromium runtime for current Home Assistant versions.
|
||||||
|
- Added clean container shutdown and safer session reset behavior.
|
||||||
|
|
||||||
## Release 1.3.2 - 2026-06-15
|
## Release 1.3.2 - 2026-06-15
|
||||||
- Fix "Freezing State" and some other improvements
|
- Fix "Freezing State" and some other improvements
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.19
|
FROM alpine:3.22
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
nodejs \
|
nodejs \
|
||||||
@@ -13,13 +13,13 @@ RUN apk add --no-cache \
|
|||||||
bash \
|
bash \
|
||||||
curl
|
curl
|
||||||
|
|
||||||
ENV CHROME_BIN=/usr/bin/chromium-browser \
|
ENV CHROME_BIN=/usr/bin/chromium \
|
||||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
RUN npm install --production
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: "WhatsApp Bridge"
|
name: "WhatsApp Bridge"
|
||||||
description: "WhatsApp Gateway with Management UI for Home Assistant"
|
description: "WhatsApp Gateway with Management UI for Home Assistant"
|
||||||
version: "1.3.2"
|
version: "1.4.0"
|
||||||
slug: "whatsapp_bridge"
|
slug: "whatsapp_bridge"
|
||||||
arch:
|
arch:
|
||||||
- aarch64
|
- aarch64
|
||||||
@@ -9,11 +9,6 @@ init: false
|
|||||||
ingress: true
|
ingress: true
|
||||||
ingress_port: 3000
|
ingress_port: 3000
|
||||||
panel_icon: "mdi:whatsapp"
|
panel_icon: "mdi:whatsapp"
|
||||||
map:
|
|
||||||
- config:rw
|
|
||||||
- share:rw
|
|
||||||
- ssl:ro
|
|
||||||
- data:rw
|
|
||||||
options:
|
options:
|
||||||
log_level: "info"
|
log_level: "info"
|
||||||
schema:
|
schema:
|
||||||
|
|||||||
Generated
+3535
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "whatsapp-bridge",
|
"name": "whatsapp-bridge",
|
||||||
"version": "1.3.2",
|
"version": "1.4.0",
|
||||||
"description": "WhatsApp bridge for Home Assistant",
|
"description": "WhatsApp bridge for Home Assistant",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"whatsapp-web.js": "^1.23.0",
|
"whatsapp-web.js": "1.34.7",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"fs-extra": "^11.1.1"
|
"fs-extra": "^11.3.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"check": "node --check server.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo "Starting WhatsApp Bridge 1.3.X..."
|
echo "Starting WhatsApp Bridge 1.4.0..."
|
||||||
node /app/server.js
|
exec node /app/server.js
|
||||||
|
|||||||
+299
-122
@@ -4,176 +4,353 @@ const qrcode = require('qrcode');
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
const PORT = 3000;
|
||||||
|
const SESSION_DATA = '/data/auth';
|
||||||
|
const CLIENT_ID = 'ha_bridge_session';
|
||||||
|
const RECONNECT_BASE_DELAY_MS = 2_000;
|
||||||
|
const RECONNECT_MAX_DELAY_MS = 60_000;
|
||||||
|
const HEALTH_CHECK_INTERVAL_MS = 30_000;
|
||||||
|
const HEALTH_CHECK_FAILURE_LIMIT = 3;
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static(path.join(__dirname, 'ui')));
|
app.use(express.static(path.join(__dirname, 'ui')));
|
||||||
|
|
||||||
let lastQr = "";
|
let client = null;
|
||||||
|
let server = null;
|
||||||
|
let reconnectTimer = null;
|
||||||
|
let reconnectAttempts = 0;
|
||||||
|
let healthCheckFailures = 0;
|
||||||
|
let clientGeneration = 0;
|
||||||
let isReady = false;
|
let isReady = false;
|
||||||
let isInitializing = true;
|
let isInitializing = true;
|
||||||
|
let isShuttingDown = false;
|
||||||
|
let lastQr = '';
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
const SESSION_DATA = '/data/auth';
|
function errorMessage(error) {
|
||||||
|
if (error instanceof Error) return error.message;
|
||||||
// --- AUTOMATISCHE SELBSTREINIGUNG BEIM START ---
|
if (typeof error === 'string') return error;
|
||||||
// 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 {
|
try {
|
||||||
console.log('Versuche Browser sauber zu schließen...');
|
return JSON.stringify(error);
|
||||||
if (client && client.pupBrowser) {
|
} catch (_error) {
|
||||||
await client.pupBrowser.close().catch(() => {});
|
return String(error);
|
||||||
}
|
|
||||||
} 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) => {
|
function findChromium() {
|
||||||
if (err.message.includes('ProtocolError') || err.message.includes('Runtime.callFunctionOn') || err.message.includes('Target closed')) {
|
const candidates = [
|
||||||
await handleCrash('Puppeteer Protocol Timeout / Crash');
|
process.env.CHROME_BIN,
|
||||||
} else {
|
'/usr/bin/chromium-browser',
|
||||||
console.error('Uncaught Exception:', err);
|
'/usr/bin/chromium'
|
||||||
}
|
].filter(Boolean);
|
||||||
});
|
|
||||||
|
|
||||||
process.on('unhandledRejection', async (reason, promise) => {
|
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
|
||||||
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({
|
// A container restart can leave Chromium lock files in the persistent profile.
|
||||||
authStrategy: new LocalAuth({
|
function removeStaleChromiumLocks() {
|
||||||
dataPath: SESSION_DATA,
|
const sessionFolder = path.join(SESSION_DATA, `session-${CLIENT_ID}`);
|
||||||
clientId: "ha_bridge_session"
|
for (const lockName of ['SingletonLock', 'SingletonCookie', 'SingletonSocket']) {
|
||||||
}),
|
const lockPath = path.join(sessionFolder, lockName);
|
||||||
authTimeoutMs: 90000,
|
try {
|
||||||
qrMaxRetries: 5,
|
if (fs.existsSync(lockPath)) fs.removeSync(lockPath);
|
||||||
puppeteer: {
|
} catch (error) {
|
||||||
executablePath: '/usr/bin/chromium-browser',
|
console.warn(`Veraltete Chromium-Sperre ${lockName} konnte nicht entfernt werden: ${errorMessage(error)}`);
|
||||||
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!
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function createClient() {
|
||||||
|
return new Client({
|
||||||
|
authStrategy: new LocalAuth({
|
||||||
|
dataPath: SESSION_DATA,
|
||||||
|
clientId: CLIENT_ID
|
||||||
|
}),
|
||||||
|
authTimeoutMs: 120_000,
|
||||||
|
// 0 disables the limit. QR codes rotate regularly while waiting for a scan;
|
||||||
|
// this is not a connection failure and must never restart the add-on.
|
||||||
|
qrMaxRetries: 0,
|
||||||
|
takeoverOnConflict: true,
|
||||||
|
takeoverTimeoutMs: 10_000,
|
||||||
|
puppeteer: {
|
||||||
|
executablePath: findChromium(),
|
||||||
|
protocolTimeout: 120_000,
|
||||||
|
headless: true,
|
||||||
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
'--disable-gpu',
|
||||||
|
'--disable-extensions',
|
||||||
|
'--no-first-run',
|
||||||
|
'--no-default-browser-check'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function destroyClient(instance) {
|
||||||
|
if (!instance) return;
|
||||||
|
instance.removeAllListeners();
|
||||||
|
try {
|
||||||
|
await instance.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`WhatsApp Client konnte nicht vollständig beendet werden: ${errorMessage(error)}`);
|
||||||
|
try {
|
||||||
|
if (instance.pupBrowser) await instance.pupBrowser.close();
|
||||||
|
} catch (_error) {
|
||||||
|
// The browser is already gone.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleReconnect(reason) {
|
||||||
|
if (isShuttingDown || reconnectTimer) return;
|
||||||
|
|
||||||
client.on('qr', (qr) => {
|
|
||||||
lastQr = qr;
|
|
||||||
isReady = false;
|
isReady = false;
|
||||||
isInitializing = false;
|
isInitializing = false;
|
||||||
console.log('Neuer QR Code generiert.');
|
lastQr = '';
|
||||||
|
lastError = errorMessage(reason);
|
||||||
|
|
||||||
|
const delay = Math.min(
|
||||||
|
RECONNECT_BASE_DELAY_MS * (2 ** reconnectAttempts),
|
||||||
|
RECONNECT_MAX_DELAY_MS
|
||||||
|
);
|
||||||
|
reconnectAttempts += 1;
|
||||||
|
|
||||||
|
console.warn(`WhatsApp Verbindung unterbrochen: ${lastError}. Neuer Versuch in ${Math.round(delay / 1000)} s.`);
|
||||||
|
reconnectTimer = setTimeout(() => {
|
||||||
|
reconnectTimer = null;
|
||||||
|
initializeClient().catch((error) => scheduleReconnect(error));
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeClient() {
|
||||||
|
if (isShuttingDown) return;
|
||||||
|
|
||||||
|
const generation = ++clientGeneration;
|
||||||
|
const previousClient = client;
|
||||||
|
client = null;
|
||||||
|
await destroyClient(previousClient);
|
||||||
|
removeStaleChromiumLocks();
|
||||||
|
|
||||||
|
if (isShuttingDown || generation !== clientGeneration) return;
|
||||||
|
|
||||||
|
const instance = createClient();
|
||||||
|
client = instance;
|
||||||
|
isReady = false;
|
||||||
|
isInitializing = true;
|
||||||
|
lastQr = '';
|
||||||
|
|
||||||
|
const isCurrent = () => !isShuttingDown && client === instance && generation === clientGeneration;
|
||||||
|
|
||||||
|
instance.on('qr', (qr) => {
|
||||||
|
if (!isCurrent()) return;
|
||||||
|
lastQr = qr;
|
||||||
|
isReady = false;
|
||||||
|
isInitializing = false;
|
||||||
|
lastError = null;
|
||||||
|
console.log('Neuer QR Code generiert. Bitte im Add-on-Webinterface scannen.');
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.on('authenticated', () => {
|
||||||
|
if (!isCurrent()) return;
|
||||||
|
lastQr = '';
|
||||||
|
isInitializing = true;
|
||||||
|
lastError = null;
|
||||||
|
console.log('WhatsApp Anmeldung erfolgreich. Verbindung wird aufgebaut...');
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.on('ready', () => {
|
||||||
|
if (!isCurrent()) return;
|
||||||
|
if (reconnectTimer) {
|
||||||
|
clearTimeout(reconnectTimer);
|
||||||
|
reconnectTimer = null;
|
||||||
|
}
|
||||||
|
isReady = true;
|
||||||
|
isInitializing = false;
|
||||||
|
lastQr = '';
|
||||||
|
lastError = null;
|
||||||
|
reconnectAttempts = 0;
|
||||||
|
healthCheckFailures = 0;
|
||||||
|
console.log('WhatsApp ist bereit!');
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.on('auth_failure', (message) => {
|
||||||
|
if (!isCurrent()) return;
|
||||||
|
console.error(`WhatsApp Authentifizierung fehlgeschlagen: ${errorMessage(message)}`);
|
||||||
|
scheduleReconnect(`Authentifizierung fehlgeschlagen: ${errorMessage(message)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.on('disconnected', (reason) => {
|
||||||
|
if (!isCurrent()) return;
|
||||||
|
scheduleReconnect(reason || 'Unbekannter Verbindungsabbruch');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Initialisiere WhatsApp Client (Chromium: ${findChromium()})...`);
|
||||||
|
try {
|
||||||
|
await instance.initialize();
|
||||||
|
} catch (error) {
|
||||||
|
if (isCurrent()) scheduleReconnect(`Initialisierungsfehler: ${errorMessage(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBrowserError(error) {
|
||||||
|
const message = errorMessage(error);
|
||||||
|
return [
|
||||||
|
'ProtocolError',
|
||||||
|
'Runtime.callFunctionOn',
|
||||||
|
'Target closed',
|
||||||
|
'Session closed',
|
||||||
|
'Execution context was destroyed'
|
||||||
|
].some((fragment) => message.includes(fragment));
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
if (isBrowserError(error)) {
|
||||||
|
scheduleReconnect(`Chromium-Fehler: ${errorMessage(error)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error('Nicht behandelter Fehler:', error);
|
||||||
|
shutdown(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('ready', () => {
|
process.on('unhandledRejection', (reason) => {
|
||||||
isReady = true;
|
if (isBrowserError(reason)) {
|
||||||
lastQr = "";
|
scheduleReconnect(`Chromium-Fehler: ${errorMessage(reason)}`);
|
||||||
isInitializing = false;
|
return;
|
||||||
console.log('WhatsApp ist bereit!');
|
}
|
||||||
|
console.error('Nicht behandelte Promise-Ablehnung:', reason);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('disconnected', async (reason) => {
|
// whatsapp-web.js normally emits "disconnected". Chromium can occasionally
|
||||||
console.log(`WhatsApp Verbindung getrennt (Grund: ${reason}).`);
|
// become unresponsive without emitting it, so verify the live state as well.
|
||||||
await handleCrash('Externer Disconnect');
|
const healthCheckTimer = setInterval(async () => {
|
||||||
});
|
if (!isReady || !client || isShuttingDown || reconnectTimer) return;
|
||||||
|
|
||||||
// --- ROUTES ---
|
const checkedClient = client;
|
||||||
|
try {
|
||||||
|
const state = await checkedClient.getState();
|
||||||
|
if (client !== checkedClient) return;
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
if (state === 'CONNECTED') {
|
||||||
|
healthCheckFailures = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
healthCheckFailures += 1;
|
||||||
|
console.warn(`WhatsApp Zustandsprüfung ${healthCheckFailures}/${HEALTH_CHECK_FAILURE_LIMIT}: ${state || 'keine Antwort'}`);
|
||||||
|
} catch (error) {
|
||||||
|
if (client !== checkedClient) return;
|
||||||
|
healthCheckFailures += 1;
|
||||||
|
console.warn(`WhatsApp Zustandsprüfung ${healthCheckFailures}/${HEALTH_CHECK_FAILURE_LIMIT} fehlgeschlagen: ${errorMessage(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (healthCheckFailures >= HEALTH_CHECK_FAILURE_LIMIT) {
|
||||||
|
healthCheckFailures = 0;
|
||||||
|
scheduleReconnect('Verbindung reagiert seit 90 Sekunden nicht');
|
||||||
|
}
|
||||||
|
}, HEALTH_CHECK_INTERVAL_MS);
|
||||||
|
healthCheckTimer.unref();
|
||||||
|
|
||||||
|
app.get('/', (_req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'ui', 'index.html'));
|
res.sendFile(path.join(__dirname, 'ui', 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/status', async (req, res) => {
|
app.get('/api/health', (_req, res) => {
|
||||||
try {
|
res.json({ status: 'ok' });
|
||||||
let qrData = null;
|
});
|
||||||
if (lastQr) {
|
|
||||||
qrData = await qrcode.toDataURL(lastQr);
|
|
||||||
}
|
|
||||||
|
|
||||||
let clientState = 'DISCONNECTED';
|
app.get('/api/status', async (_req, res) => {
|
||||||
if (isReady) {
|
try {
|
||||||
clientState = await client.getState().catch(() => 'ERROR');
|
const qrData = lastQr ? await qrcode.toDataURL(lastQr) : null;
|
||||||
}
|
let clientState = isInitializing ? 'INITIALIZING' : 'DISCONNECTED';
|
||||||
|
if (isReady && client) clientState = await client.getState().catch(() => 'ERROR');
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
isReady: isReady,
|
isReady,
|
||||||
hasQr: lastQr !== "",
|
hasQr: Boolean(lastQr),
|
||||||
qrCode: qrData,
|
qrCode: qrData,
|
||||||
isInitializing: isInitializing,
|
isInitializing,
|
||||||
clientState: clientState
|
clientState,
|
||||||
|
reconnectAttempts,
|
||||||
|
lastError
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: e.message });
|
res.status(500).json({ error: errorMessage(error) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTFALL-SICHER: Löscht die Session radikal, selbst wenn der Client eingefroren ist!
|
app.post('/auth/logout', async (_req, res) => {
|
||||||
app.post('/auth/logout', async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
console.log("Logout erzwungen über Web-UI...");
|
console.log('Logout über Web-UI angefordert...');
|
||||||
if (client) {
|
isReady = false;
|
||||||
await client.logout().catch(() => {});
|
clientGeneration += 1;
|
||||||
await client.destroy().catch(() => {});
|
const oldClient = client;
|
||||||
}
|
client = null;
|
||||||
await fs.remove(SESSION_DATA).catch(() => {});
|
if (oldClient) await oldClient.logout().catch(() => {});
|
||||||
res.json({ success: true, message: "Session radikal gelöscht. Addon startet jetzt sauber neu..." });
|
await destroyClient(oldClient);
|
||||||
setTimeout(() => process.exit(0), 1500);
|
await fs.remove(SESSION_DATA);
|
||||||
} catch (err) {
|
reconnectAttempts = 0;
|
||||||
res.status(500).json({ error: err.message });
|
res.json({ success: true, message: 'Session gelöscht. Eine neue Anmeldung wird vorbereitet.' });
|
||||||
|
setTimeout(() => initializeClient().catch((error) => scheduleReconnect(error)), 1_000);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: errorMessage(error) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTFALL-RESET: Putzt das komplette Verzeichnis leer
|
app.delete('/system/reset', async (_req, res) => {
|
||||||
app.delete('/system/reset', async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
console.log("Factory Reset erzwungen...");
|
console.log('Werkseinstellungen über Web-UI angefordert...');
|
||||||
if (client) await client.destroy().catch(() => {});
|
isReady = false;
|
||||||
await fs.emptyDir('/data').catch(() => {});
|
clientGeneration += 1;
|
||||||
res.json({ success: true, message: "Werkseinstellungen erzwungen. Alles gelöscht. Neustart..." });
|
const oldClient = client;
|
||||||
setTimeout(() => process.exit(0), 1500);
|
client = null;
|
||||||
} catch (err) {
|
await destroyClient(oldClient);
|
||||||
res.status(500).json({ error: err.message });
|
// Only remove bridge-owned data. options.json belongs to Home Assistant.
|
||||||
|
await fs.remove(SESSION_DATA);
|
||||||
|
await fs.remove('/data/.wwebjs_cache');
|
||||||
|
reconnectAttempts = 0;
|
||||||
|
res.json({ success: true, message: 'WhatsApp-Session und Web-Cache gelöscht. Neue Anmeldung wird vorbereitet.' });
|
||||||
|
setTimeout(() => initializeClient().catch((error) => scheduleReconnect(error)), 1_000);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: errorMessage(error) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/send', async (req, res) => {
|
app.post('/send', async (req, res) => {
|
||||||
const { number, message } = req.body;
|
const { number, message } = req.body;
|
||||||
if (!isReady) return res.status(503).json({ error: 'Client nicht bereit' });
|
if (!number || typeof message !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'number und message sind erforderlich' });
|
||||||
|
}
|
||||||
|
if (!isReady || !client) return res.status(503).json({ error: 'Client nicht bereit' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const chatId = number.includes('@c.us') ? number : `${number}@c.us`;
|
const chatId = number.includes('@c.us') ? number : `${number}@c.us`;
|
||||||
await client.sendMessage(chatId, message);
|
await client.sendMessage(chatId, message);
|
||||||
res.json({ success: true });
|
return res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: err.message });
|
return res.status(500).json({ error: errorMessage(error) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Initialisiere WhatsApp Client...');
|
async function shutdown(exitCode = 0) {
|
||||||
client.initialize().catch(async (err) => {
|
if (isShuttingDown) return;
|
||||||
await handleCrash(`Initialisierungsfehler: ${err.message}`);
|
isShuttingDown = true;
|
||||||
});
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||||
|
clearInterval(healthCheckTimer);
|
||||||
|
console.log('WhatsApp Bridge wird sauber beendet...');
|
||||||
|
|
||||||
app.listen(3000, () => console.log('WhatsApp Bridge Server läuft auf Port 3000'));
|
if (server) await new Promise((resolve) => server.close(resolve));
|
||||||
|
const oldClient = client;
|
||||||
|
client = null;
|
||||||
|
await destroyClient(oldClient);
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => shutdown(0));
|
||||||
|
process.on('SIGINT', () => shutdown(0));
|
||||||
|
|
||||||
|
server = app.listen(PORT, () => console.log(`WhatsApp Bridge Server läuft auf Port ${PORT}`));
|
||||||
|
initializeClient().catch((error) => scheduleReconnect(error));
|
||||||
|
|||||||
@@ -47,12 +47,15 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function update() {
|
async function update() {
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const msgEl = document.getElementById('msg');
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getBaseUrl() + 'api/status');
|
const response = await fetch(getBaseUrl() + 'api/status');
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const statusEl = document.getElementById('status');
|
|
||||||
const qrSection = document.getElementById('qr-section');
|
const qrSection = document.getElementById('qr-section');
|
||||||
|
msgEl.innerText = data.lastError || '';
|
||||||
|
|
||||||
if (data.isReady) {
|
if (data.isReady) {
|
||||||
statusEl.innerText = "VERBUNDEN ✅";
|
statusEl.innerText = "VERBUNDEN ✅";
|
||||||
@@ -67,6 +70,10 @@
|
|||||||
statusEl.innerText = "LADE CLIENT... ⏳";
|
statusEl.innerText = "LADE CLIENT... ⏳";
|
||||||
statusEl.className = "status-badge loading";
|
statusEl.className = "status-badge loading";
|
||||||
qrSection.style.display = "none";
|
qrSection.style.display = "none";
|
||||||
|
} else if (data.lastError) {
|
||||||
|
statusEl.innerText = `NEUVERBINDUNG (${data.reconnectAttempts}) ⏳`;
|
||||||
|
statusEl.className = "status-badge loading";
|
||||||
|
qrSection.style.display = "none";
|
||||||
} else {
|
} else {
|
||||||
statusEl.innerText = "INITIALISIERE...";
|
statusEl.innerText = "INITIALISIERE...";
|
||||||
statusEl.className = "status-badge loading";
|
statusEl.className = "status-badge loading";
|
||||||
@@ -75,6 +82,7 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
statusEl.innerText = "VERBINDUNG VERLOREN ❌";
|
statusEl.innerText = "VERBINDUNG VERLOREN ❌";
|
||||||
statusEl.className = "status-badge disconnected";
|
statusEl.className = "status-badge disconnected";
|
||||||
|
msgEl.innerText = e.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user