Files
whatsapp-reminder/src/index.js
T
2026-05-28 11:19:11 +02:00

236 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { Client, LocalAuth } = require('whatsapp-web.js');
const qrcode = require('qrcode-terminal');
const qrcodeLib = require('qrcode');
const cron = require('node-cron');
const http = require('http');
const fs = require('fs');
const path = require('path');
const CONTACTS_PATH = path.join(__dirname, '..', 'config', 'contacts.json');
const SETTINGS_PATH = path.join(__dirname, '..', 'config', 'settings.json');
const DATA_PATH = path.join(__dirname, '..', 'data');
let currentQR = null;
let isReady = false;
function loadSettings() {
return JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
}
function loadContacts() {
return JSON.parse(fs.readFileSync(CONTACTS_PATH, 'utf8'));
}
// Gibt den Kontakt zurück der im angegebenen Monat dran ist (wiederholt sich jedes Jahr).
function getContactForMonth(year, month) {
const settings = loadSettings();
const contacts = loadContacts();
const schedule = settings.schedule;
if (!Array.isArray(schedule) || schedule.length !== 12) {
console.log('schedule in settings.json muss ein Array mit 12 Einträgen sein.');
return null;
}
const contactId = schedule[month - 1];
if (!contactId) {
console.log(`Monat ${month}/${year}: kein Kontakt (null) — nichts zu tun.`);
return null;
}
const contact = contacts.find(c => c.id === contactId);
if (!contact) {
console.log(`Kontakt-ID "${contactId}" nicht in contacts.json gefunden.`);
return null;
}
return contact;
}
async function sendReminders(overrideYear = null, overrideMonth = null) {
const settings = loadSettings();
const now = new Date();
const year = overrideYear ?? now.getFullYear();
const month = overrideMonth ?? (now.getMonth() + 1);
const monthName = new Date(year, month - 1, 1).toLocaleString('de-DE', { month: 'long' });
const contact = getContactForMonth(year, month);
if (!contact) {
return { skipped: true, reason: `Kein Kontakt für ${monthName} ${year}` };
}
const vorname = contact.name.split(' ')[0];
const message = settings.message
.replace(/{vorname}/g, vorname)
.replace(/{name}/g, contact.name)
.replace(/{amount}/g, settings.amount ?? '')
.replace(/{currency}/g, settings.currency ?? '')
.replace(/{month}/g, monthName)
.replace(/{year}/g, year);
const phone = contact.phone.replace(/\D/g, '');
const chatId = `${phone}@c.us`;
console.log(`[${monthName} ${year}] Sende an: ${contact.name} (${contact.phone})`);
try {
await client.sendMessage(chatId, message);
console.log(`[OK] Nachricht gesendet.`);
return { sent: true, contact: contact.name };
} catch (err) {
console.error(`[FEHLER] ${err.message}`);
return { sent: false, error: err.message };
}
}
// Web-Interface
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, 'http://localhost');
if (url.pathname === '/qr') {
if (!currentQR) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(htmlPage('Status', isReady
? '<p style="color:green;font-size:1.1em">✓ Verbunden und bereit</p><p><a href="/send-now">Erinnerung jetzt senden</a></p>'
: '<p>Kein QR-Code verfügbar — Client startet noch, bitte kurz warten...</p>'
));
}
const qrDataUrl = await qrcodeLib.toDataURL(currentQR);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(htmlPage('QR-Code scannen', `
<p>Öffne WhatsApp &rarr; <strong>Verknüpfte Geräte</strong> &rarr; <strong>Gerät verknüpfen</strong></p>
<img src="${qrDataUrl}" style="width:280px;height:280px;border:1px solid #ddd;border-radius:8px;" />
<p><small>Seite neu laden falls der Code abgelaufen ist.</small></p>
`));
}
if (url.pathname === '/send-now') {
if (!isReady) {
res.writeHead(503, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(htmlPage('Nicht bereit', '<p>Client ist noch nicht verbunden. Erst QR-Code scannen.</p>'));
}
// Optionale Query-Parameter: ?year=2026&month=5
const year = url.searchParams.has('year') ? parseInt(url.searchParams.get('year')) : null;
const month = url.searchParams.has('month') ? parseInt(url.searchParams.get('month')) : null;
const result = await sendReminders(year, month);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
if (result.skipped) {
return res.end(htmlPage('Übersprungen', `<p style="color:orange">⏭ ${result.reason}</p><p><a href="/">Zurück</a></p>`));
}
return res.end(htmlPage(result.sent ? 'Gesendet' : 'Fehler', `
${result.sent
? `<p style="color:green">✓ Nachricht an ${result.contact} gesendet</p>`
: `<p style="color:red">✗ Fehler: ${result.error}</p>`}
<p><a href="/">Zurück</a></p>
`));
}
if (url.pathname === '/status') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ ready: isReady, qr_pending: !!currentQR }));
}
// Startseite mit Monatsübersicht
let scheduleHtml = '';
try {
const settings = loadSettings();
const contacts = loadContacts();
const now = new Date();
const thisYear = now.getFullYear();
const thisMonth = now.getMonth() + 1;
const monthNames = ['', 'Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const plan = settings.schedule ?? [];
const rows = plan.map((id, i) => {
const m = i + 1;
const contact = id ? contacts.find(c => c.id === id) : null;
const isCurrent = m === thisMonth;
return `<tr style="${isCurrent ? 'font-weight:bold;background:#e7f7f0' : ''}">
<td style="padding:4px 12px">${monthNames[m]}</td>
<td style="padding:4px 12px">${contact ? contact.name : '—'}</td>
${isCurrent ? '<td style="padding:4px 12px;color:green">← aktueller Monat</td>' : '<td></td>'}
</tr>`;
}).join('');
scheduleHtml = `<h2>Plan ${thisYear}</h2><table border="0" cellspacing="0">${rows}</table>`;
} catch (e) {
scheduleHtml = `<p style="color:red">Fehler beim Laden des Plans: ${e.message}</p>`;
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(htmlPage('WhatsApp Reminder', `
<p>Status: ${isReady
? '<span style="color:green">✓ Verbunden</span>'
: currentQR
? '<span style="color:orange"><a href="/qr">QR-Code scannen</a></span>'
: '<span style="color:gray">Startet...</span>'}</p>
<p><a href="/send-now">▶ Erinnerung für diesen Monat jetzt senden</a></p>
${scheduleHtml}
`));
});
function htmlPage(title, body) {
return `<!DOCTYPE html><html><head>
<meta charset="utf-8"><title>${title} WhatsApp Reminder</title>
<style>body{font-family:sans-serif;max-width:650px;margin:40px auto;padding:0 20px}
a{color:#075e54}h1,h2{color:#075e54}table{border-collapse:collapse}
td{border-bottom:1px solid #eee}</style>
</head><body><h1>${title}</h1>${body}</body></html>`;
}
server.listen(3000, () => {
console.log('Web-Interface: http://localhost:3000');
console.log('QR-Code: http://localhost:3000/qr');
});
// WhatsApp Client
const client = new Client({
authStrategy: new LocalAuth({ dataPath: DATA_PATH }),
puppeteer: {
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--single-process',
'--disable-gpu'
]
}
});
client.on('qr', (qr) => {
currentQR = qr;
console.log('\n>>> QR-Code bereit: http://localhost:3000/qr <<<');
qrcode.generate(qr, { small: true });
});
client.on('authenticated', () => {
currentQR = null;
console.log('Authentifiziert.');
});
client.on('ready', () => {
isReady = true;
const settings = loadSettings();
const schedule = settings.cron || '0 9 1 * *';
const tz = settings.timezone || 'Europe/Zurich';
cron.schedule(schedule, async () => {
const now = new Date();
console.log(`[CRON] ${now.toLocaleString('de-DE')} — prüfe Monatsplan...`);
await sendReminders();
}, { timezone: tz });
console.log(`WhatsApp bereit. Cron: "${schedule}" (${tz})`);
});
client.on('disconnected', (reason) => {
console.log('Verbindung getrennt:', reason);
isReady = false;
process.exit(1);
});
client.initialize();