const express = require('express'); const cron = require('node-cron'); const fs = require('fs'); const http = require('http'); const path = require('path'); const app = express(); app.use(express.json()); const DATA_FILE = process.env.DATA_FILE || '/data/reminders.json'; const GATEWAY = process.env.GATEWAY_URL || 'http://whatsapp-gateway.ai-agents.svc.cluster.local:5001/send-message'; const PERSONAL = process.env.PERSONAL_NUM || '+14085505485'; const GROUP = process.env.GROUP_URL || 'https://chat.whatsapp.com/FXO0SCLA8DpJwClNNWdhhx'; const PORT = parseInt(process.env.PORT) || 6001; // ── Persistence ────────────────────────────────────────────────────────────── function load() { try { return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); } catch { return []; } } function save(reminders) { fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true }); fs.writeFileSync(DATA_FILE, JSON.stringify(reminders, null, 2)); } // ── WhatsApp delivery ───────────────────────────────────────────────────────── function sendWhatsApp(to, message) { return new Promise((resolve) => { const body = JSON.stringify({ to, message }); const url = new URL(GATEWAY); const opts = { hostname: url.hostname, port: parseInt(url.port) || 80, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }; const req = http.request(opts, r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({ success: false, error: 'parse error' }); } }); }); req.on('error', e => resolve({ success: false, error: e.message })); req.write(body); req.end(); }); } // ── Routes ──────────────────────────────────────────────────────────────────── // POST /reminders — schedule a new reminder app.post('/reminders', (req, res) => { const { message, sendAt, to, label } = req.body; if (!message || !sendAt) { return res.status(400).json({ error: 'message and sendAt are required' }); } const sendAtDate = new Date(sendAt); if (isNaN(sendAtDate.getTime())) { return res.status(400).json({ error: 'sendAt is not a valid ISO date string' }); } if (sendAtDate <= new Date()) { return res.status(400).json({ error: 'sendAt must be in the future' }); } const reminders = load(); const id = `rem_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; reminders.push({ id, message, sendAt: sendAtDate.toISOString(), to: to || PERSONAL, label: label || message.slice(0, 60), status: 'pending', createdAt: new Date().toISOString(), }); save(reminders); console.log(`[Reminder] Scheduled ${id}: "${label || message.slice(0, 40)}" → ${sendAtDate.toISOString()}`); res.json({ success: true, id, sendAt: sendAtDate.toISOString() }); }); // POST /reminders/batch — schedule multiple reminders at once app.post('/reminders/batch', (req, res) => { const { items } = req.body; // [{ message, sendAt, to, label }] if (!Array.isArray(items) || items.length === 0) { return res.status(400).json({ error: 'items array is required' }); } const reminders = load(); const created = []; const errors = []; for (const item of items) { const { message, sendAt, to, label } = item; if (!message || !sendAt) { errors.push({ item, error: 'missing fields' }); continue; } const sendAtDate = new Date(sendAt); if (isNaN(sendAtDate.getTime()) || sendAtDate <= new Date()) { errors.push({ item, error: 'invalid or past sendAt' }); continue; } const id = `rem_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; reminders.push({ id, message, sendAt: sendAtDate.toISOString(), to: to || PERSONAL, label: label || message.slice(0, 60), status: 'pending', createdAt: new Date().toISOString(), }); created.push({ id, sendAt: sendAtDate.toISOString(), label: label || message.slice(0, 40) }); } save(reminders); console.log(`[Reminder] Batch: ${created.length} scheduled, ${errors.length} failed`); res.json({ success: true, created, errors }); }); // GET /reminders — list reminders app.get('/reminders', (req, res) => { const all = load(); const status = req.query.status; // optional filter: pending|sent|cancelled|failed const result = status ? all.filter(r => r.status === status) : all; res.json(result.slice(-100)); // last 100 }); // DELETE /reminders/:id — cancel a reminder app.delete('/reminders/:id', (req, res) => { const reminders = load(); const idx = reminders.findIndex(r => r.id === req.params.id); if (idx === -1) return res.status(404).json({ error: 'not found' }); if (reminders[idx].status !== 'pending') { return res.status(409).json({ error: `Cannot cancel — status is "${reminders[idx].status}"` }); } reminders[idx].status = 'cancelled'; save(reminders); console.log(`[Reminder] Cancelled ${req.params.id}`); res.json({ success: true }); }); // GET /health app.get('/health', (req, res) => res.json({ status: 'ok', time: new Date().toISOString() })); // ── Scheduler (every minute) ────────────────────────────────────────────────── cron.schedule('* * * * *', async () => { const now = new Date(); const reminders = load(); let changed = false; for (const r of reminders) { if (r.status !== 'pending') continue; if (new Date(r.sendAt) > now) continue; console.log(`[Reminder] Firing ${r.id}: "${r.label}" → ${r.to}`); const result = await sendWhatsApp(r.to, r.message); r.status = result.success ? 'sent' : 'failed'; r.sentAt = now.toISOString(); if (!result.success) r.error = result.error; changed = true; console.log(`[Reminder] ${result.success ? '✅' : '❌'} ${r.id} ${result.success ? result.messageId : r.error}`); } if (changed) save(reminders); }); app.listen(PORT, () => { console.log(`[ReminderService] Listening on :${PORT}`); console.log(`[ReminderService] Data: ${DATA_FILE} | Gateway: ${GATEWAY}`); const pending = load().filter(r => r.status === 'pending').length; console.log(`[ReminderService] Loaded ${pending} pending reminder(s).`); });