Initial commit

This commit is contained in:
2026-05-28 11:11:27 +02:00
commit d8a626f6c3
8 changed files with 514 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
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();