Initial commit
This commit is contained in:
+235
@@ -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 → <strong>Verknüpfte Geräte</strong> → <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();
|
||||
Reference in New Issue
Block a user