183 lines
6.6 KiB
JavaScript
183 lines
6.6 KiB
JavaScript
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;
|
|
const GROUP = process.env.GROUP_URL;
|
|
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).`);
|
|
});
|