/** * MADOKA BOT - V69 (Post-Message Anti-Enterramiento para Cierres) * CRM Quantico + Auto Media + Exact Docs + Groq + RAG */ const express = require('express'); const path = require('path'); const fs = require('fs'); const fetch = require('node-fetch'); const http = require('http'); const { Server } = require('socket.io'); const qrcode = require('qrcode'); const { Client, LocalAuth, MessageMedia } = require('whatsapp-web.js'); let pdfParse; try { pdfParse = require('pdf-parse'); } catch(e) { console.log("⚠️ Módulo 'pdf-parse' no encontrado."); } const ROOT = __dirname; const DEBUG_FILE = path.join(ROOT, 'DEBUG_LOCAL.txt'); const START_TIME = Date.now(); const SYSTEM_VERSION = "MADOKA V69.0 (Quantico System)"; const log = (msg) => { const time = new Date().toLocaleString('es-DO', { timeZone: 'America/Santo_Domingo' }); const line = `[${time}] ${msg}\n`; fs.appendFileSync(DEBUG_FILE, line); console.log(line); }; if (fs.existsSync(DEBUG_FILE)) fs.writeFileSync(DEBUG_FILE, ""); process.on('uncaughtException', err => log(`[FATAL UNCAUGHT] ${err.stack}`)); process.on('unhandledRejection', err => log(`[FATAL REJECTION] ${err.stack || err}`)); log("--- INICIANDO MADOKA V69 (QUANTICO RESTAURANT) ---"); try { require('dotenv').config({ path: path.join(ROOT, '.env') }); } catch (e) {} const app = express(); const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*", methods: ["GET", "POST", "PUT", "DELETE"], allowedHeaders: ["ngrok-skip-browser-warning", "Content-Type"] } }); const PORT = process.env.PORT || 3000; app.use(require('cors')({ origin: "*", allowedHeaders: ['Content-Type', 'ngrok-skip-browser-warning', 'Authorization'] })); const AUTH_DIR = path.join(ROOT, 'madoka_auth'); const DATA_DIR = path.join(ROOT, 'data'); const PUBLIC_DIR = path.join(ROOT, 'public'); const MEDIA_DIR = path.join(ROOT, 'media'); const DOCS_DIR = path.join(MEDIA_DIR, 'docs'); [AUTH_DIR, DATA_DIR, PUBLIC_DIR, MEDIA_DIR, DOCS_DIR].forEach(dir => { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); }); const FILES = { STATS: path.join(DATA_DIR, 'stats.json'), RULES: path.join(DATA_DIR, 'rules.json'), LOGS: path.join(DATA_DIR, 'logs.json'), USERS: path.join(DATA_DIR, 'users.json'), CONTEXT: path.join(DATA_DIR, 'context.txt'), REMINDERS: path.join(DATA_DIR, 'reminders.json'), CONTACTS: path.join(DATA_DIR, 'contacts.json'), COMMANDS: path.join(DATA_DIR, 'commands.json'), SETTINGS: path.join(DATA_DIR, 'settings.json') }; // 🧠 CONFIGURACIÓN POR DEFECTO CON CONTROL DE TIEMPOS Y EMPRESA const DEFAULT_SETTINGS = { companyName: "Quantico", delayGeneral: 20000, delayInfo: 10000, delayMedia: 1500, delayClosing: 10000 }; const db = { read: (file, defaultData) => { if (!fs.existsSync(file)) fs.writeFileSync(file, JSON.stringify(defaultData, null, 2)); try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { return defaultData; } }, write: (file, data) => fs.writeFileSync(file, JSON.stringify(data, null, 2)) }; const cleanJid = (jid) => { if (!jid) return ""; if (jid.includes('status@broadcast')) return 'status@broadcast'; const num = jid.replace(/[^0-9]/g, ''); if(!num) return jid; return num + '@c.us'; }; function findFileFuzzy(dir, targetName) { if (!fs.existsSync(dir)) return null; const files = fs.readdirSync(dir); const targetClean = targetName.toLowerCase().replace('.txt', '').trim(); for (let file of files) { const fileClean = file.toLowerCase().replace('.txt', '').trim(); if (fileClean === targetClean || fileClean.includes(targetClean)) { return file; } } return null; } if (!fs.existsSync(FILES.CONTEXT)) fs.writeFileSync(FILES.CONTEXT, "Eres MADOKA."); db.read(FILES.RULES, []); db.read(FILES.LOGS, []); db.read(FILES.STATS, { total_mensajes: 0 }); db.read(FILES.REMINDERS, []); db.read(FILES.CONTACTS, []); db.read(FILES.COMMANDS, []); db.read(FILES.SETTINGS, DEFAULT_SETTINGS); let rawUsers = db.read(FILES.USERS, []); let strictUniqueUsers = {}; rawUsers.forEach(u => { let cleanId = cleanJid(u.id); if (!strictUniqueUsers[cleanId]) { u.id = cleanId; u.funnel_stage = u.funnel_stage || 'new'; u.score = u.score || 10; u.sentiment = u.sentiment || 'neutral'; u.tags = u.tags || []; u.received_media = u.received_media || []; if(u.name && u.name.includes("906") && u.name.includes("4631")) { u.name = "Usuario"; } strictUniqueUsers[cleanId] = u; } else { if (u.is_admin) strictUniqueUsers[cleanId].is_admin = true; if (u.timestamp > strictUniqueUsers[cleanId].timestamp) { strictUniqueUsers[cleanId].last_msg = u.last_msg; strictUniqueUsers[cleanId].timestamp = u.timestamp; strictUniqueUsers[cleanId].date_only = u.date_only; if(u.last_msg_id) strictUniqueUsers[cleanId].last_msg_id = u.last_msg_id; } if (u.name && !u.name.includes("906") && u.name !== "Usuario" && u.name !== strictUniqueUsers[cleanId].name) { strictUniqueUsers[cleanId].name = u.name; } else if (strictUniqueUsers[cleanId].name && strictUniqueUsers[cleanId].name.includes("906") && strictUniqueUsers[cleanId].name.includes("4631")) { strictUniqueUsers[cleanId].name = "Usuario"; } if(!strictUniqueUsers[cleanId].funnel_stage) strictUniqueUsers[cleanId].funnel_stage = 'new'; if(!strictUniqueUsers[cleanId].score) strictUniqueUsers[cleanId].score = 10; if(!strictUniqueUsers[cleanId].sentiment) strictUniqueUsers[cleanId].sentiment = 'neutral'; if(!strictUniqueUsers[cleanId].tags) strictUniqueUsers[cleanId].tags = []; if(!strictUniqueUsers[cleanId].received_media) strictUniqueUsers[cleanId].received_media = []; strictUniqueUsers[cleanId].msg_count += (u.msg_count || 1); } }); db.write(FILES.USERS, Object.values(strictUniqueUsers)); let qrDataURL = null; let isConnected = false; const client = new Client({ authStrategy: new LocalAuth({ dataPath: AUTH_DIR }), puppeteer: { headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--single-process', '--disable-gpu'] } }); async function sendSafeMessage(jid, text, originalMsgObj = null) { let finalJid = cleanJid(jid); try { if (originalMsgObj) { try { const chat = await originalMsgObj.getChat(); await chat.sendMessage(text); return; } catch(e) {} } try { const chat = await client.getChatById(finalJid); if (chat) { await chat.sendMessage(text); return; } } catch(e) {} try { const cleanNumber = finalJid.split('@')[0]; const numberId = await client.getNumberId(cleanNumber); if (numberId) { await client.sendMessage(numberId._serialized, text); return; } } catch(e) {} await client.sendMessage(finalJid, text); } catch (error) { log(`❌ Error enviando texto a ${finalJid}: ${error.message}`); throw new Error("Fallo de conexión con WhatsApp al enviar texto."); } } async function sendSafeMedia(jid, mediaToSend, originalMsgObj = null) { let finalJid = cleanJid(jid); try { if (originalMsgObj) { try { const chat = await originalMsgObj.getChat(); await chat.sendMessage(mediaToSend); return; } catch(e) {} } try { const chat = await client.getChatById(finalJid); if (chat) { await chat.sendMessage(mediaToSend); return; } } catch(e) {} try { const cleanNumber = finalJid.split('@')[0]; const numberId = await client.getNumberId(cleanNumber); if (numberId) { await client.sendMessage(numberId._serialized, mediaToSend); return; } } catch(e) {} await client.sendMessage(finalJid, mediaToSend); } catch (error) { log(`❌ Error crítico enviando media a ${finalJid}: ${error.message}`); } } app.use(express.json({ limit: '10mb' })); app.use(express.static(PUBLIC_DIR)); io.on('connection', (socket) => { socket.on('send_message_from_panel', async (data) => { if (isConnected && data.jid && data.text) { try { await sendSafeMessage(data.jid, data.text); const logs = db.read(FILES.LOGS, []); const time = new Date().toLocaleTimeString('es-DO', { timeZone: 'America/Santo_Domingo' }); const logEntry = { jid: data.jid, from: "Tú (Panel)", msg: data.text, date: time, timestamp: Date.now(), type: 'out' }; logs.push(logEntry); db.write(FILES.LOGS, logs.slice(-5000)); io.emit('new_message_update', logEntry); } catch (e) {} } }); }); app.get('/log', (req, res) => res.send(`
${fs.readFileSync(DEBUG_FILE, 'utf8')}`));
app.get('/api/settings', (req, res) => res.json(db.read(FILES.SETTINGS, DEFAULT_SETTINGS)));
app.post('/api/settings', (req, res) => {
const current = db.read(FILES.SETTINGS, DEFAULT_SETTINGS);
if(req.body.companyName !== undefined) current.companyName = req.body.companyName;
if(req.body.delayGeneral !== undefined) current.delayGeneral = req.body.delayGeneral;
if(req.body.delayInfo !== undefined) current.delayInfo = req.body.delayInfo;
if(req.body.delayMedia !== undefined) current.delayMedia = req.body.delayMedia;
if(req.body.delayClosing !== undefined) current.delayClosing = req.body.delayClosing;
db.write(FILES.SETTINGS, current);
io.emit('settings_updated', current);
res.json({ success: true });
});
app.get('/api/system-info', (req, res) => { const uptimeMs = Date.now() - START_TIME; const hours = Math.floor(uptimeMs / 3600000); const minutes = Math.floor((uptimeMs % 3600000) / 60000); const settings = db.read(FILES.SETTINGS, DEFAULT_SETTINGS); res.json({ version: SYSTEM_VERSION, company: settings.companyName, uptime: `${hours} horas, ${minutes} minutos`, status: isConnected ? "Operativo 🟢" : "Desconectado 🔴" }); });
app.post('/api/sync-contacts', async (req, res) => { if(!isConnected) return res.json({ success: false, error: "WhatsApp no está conectado." }); try { const contacts = await client.getContacts(); const saved = contacts.filter(c => c.isUser).map(c => ({ id: cleanJid(c.id._serialized), name: c.name || c.pushname || "Desconocido" })); db.write(FILES.CONTACTS, saved); res.json({ success: true, count: saved.length }); } catch(e) { res.json({ success: false, error: e.message }); } });
app.post('/api/sync-chats', async (req, res) => {
if(!isConnected) return res.json({ success: false, error: "WhatsApp no está conectado." });
try {
const chats = await client.getChats(); let users = db.read(FILES.USERS, []); let added = 0;
chats.filter(c => !c.isGroup).forEach(c => {
const cId = cleanJid(c.id._serialized);
if(!users.find(u => u.id === cId)) {
users.push({ id: cId, name: c.name || "Usuario Sync", ia_active: true, is_admin: false, date_only: new Date().toLocaleDateString('es-DO'), msg_count: 0, last_msg: "[Sincronizado]", timestamp: Date.now(), funnel_stage: 'new', score: 10, sentiment: 'neutral', tags: [], received_media: [] }); added++;
}
});
db.write(FILES.USERS, users); res.json({ success: true, count: added });
} catch(e) { res.json({ success: false, error: e.message }); }
});
app.post('/api/clear-learning', (req, res) => { const { jid } = req.body; try { if (jid === 'ALL') { db.write(FILES.LOGS, []); } else { let logs = db.read(FILES.LOGS, []); logs = logs.filter(l => l.jid !== jid); db.write(FILES.LOGS, logs); } res.json({ success: true }); } catch(e) { res.json({ success: false, error: e.message }); } });
app.delete('/api/users/:id', (req, res) => { const targetJid = req.params.id; try { let users = db.read(FILES.USERS, []); users = users.filter(u => u.id !== targetJid); db.write(FILES.USERS, users); let logs = db.read(FILES.LOGS, []); logs = logs.filter(l => l.jid !== targetJid); db.write(FILES.LOGS, logs); io.emit('user_deleted', targetJid); res.json({ success: true }); } catch(e) { res.json({ success: false, error: e.message }); } });
app.get('/api/analytics', (req, res) => { const logs = db.read(FILES.LOGS, []); const users = db.read(FILES.USERS, []); const today = new Date().toLocaleDateString('es-DO', { timeZone: 'America/Santo_Domingo' }); const chartLabels = []; const chartData = []; const daysMap = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb']; const now = new Date(); const tzOffset = now.getTimezoneOffset() * 60000; let msgsIn = 0; let msgsOut = 0; for(let i=6; i>=0; i--) { const d = new Date(now); d.setDate(d.getDate() - i); chartLabels.push(daysMap[d.getDay()]); const startOfDay = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0).getTime(); const endOfDay = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59).getTime(); const dayLogs = logs.filter(l => l.timestamp && l.timestamp >= startOfDay && l.timestamp <= endOfDay); chartData.push(dayLogs.length); if (i === 0) { msgsIn = dayLogs.filter(l => l.type === 'in').length; msgsOut = dayLogs.filter(l => l.type === 'out').length; } } const topUsers = [...users].sort((a,b) => b.msg_count - a.msg_count).slice(0, 5); res.json({ total_mensajes: logs.length, total_usuarios: users.length, hoy: users.filter(u => u.date_only === today).length, msgs_in_today: msgsIn, msgs_out_today: msgsOut, chartLabels, chartData, topUsers }); });
app.get('/api/context', (req, res) => res.json({ text: fs.readFileSync(FILES.CONTEXT, 'utf8') }));
app.post('/api/context', (req, res) => { fs.writeFileSync(FILES.CONTEXT, req.body.text); res.json({ success: true }); });
app.get('/api/logs', (req, res) => res.json(db.read(FILES.LOGS, []).slice(-1000)));
app.get('/api/rules', (req, res) => res.json(db.read(FILES.RULES, [])));
app.get('/api/users', (req, res) => res.json(db.read(FILES.USERS, [])));
app.get('/api/docs', (req, res) => { if(!fs.existsSync(DOCS_DIR)) return res.json([]); const files = fs.readdirSync(DOCS_DIR).filter(f => f.endsWith('.pdf') || f.endsWith('.txt')); res.json(files); });
app.post('/api/admin-chat', async (req, res) => { const { message, history } = req.body; const users = db.read(FILES.USERS, []).filter(u => u.id !== 'status@broadcast'); const crmData = users.map(u => ({ nombre: u.name, etapa: u.funnel_stage, calificacion_lead: u.score, humor: u.sentiment, etiquetas: u.tags, ultimo_mensaje: u.last_msg })); const systemPrompt = `Eres MADOKA, Analista de Negocios de la agencia. \nBase de Datos CRM:\n${JSON.stringify(crmData)}\nDa respuestas directas, profesionales y estratégicas. Usa viñetas y emojis.`; let messagesArray = [{ role: "system", content: systemPrompt }]; if (history && history.length > 0) messagesArray = messagesArray.concat(history); messagesArray.push({ role: "user", content: message }); try { const response = await consultarGroqBlindado(messagesArray); res.json({ success: true, response }); } catch (e) { res.json({ success: false, error: e.message }); } });
app.post('/api/summarize', async (req, res) => { const { jid } = req.body; const logs = db.read(FILES.LOGS, []).filter(l => l.jid === cleanJid(jid)).slice(-40); if(logs.length === 0) return res.json({ success: false, error: "No hay historial para resumir." }); const chatText = logs.map(l => `${l.from}: ${l.msg}`).join('\n'); const prompt = `Resume este chat de WhatsApp en 3 viñetas cortas y profesionales. Usa emojis.\n\nHistorial:\n${chatText}`; try { const summary = await consultarGroqBlindado([{ role: "user", content: prompt }]); res.json({ success: true, summary }); } catch (e) { res.json({ success: false, error: e.message }); } });
app.post('/api/force-ai-reply', async (req, res) => {
const { jid } = req.body;
if(!jid) return res.json({ success: false, error: "Falta JID" });
try {
const logs = db.read(FILES.LOGS, []);
const userHistory = logs.filter(l => l.jid === cleanJid(jid)).slice(-15).map(l => `${l.from}: ${l.msg}`).join('\n');
const ctx = fs.readFileSync(FILES.CONTEXT, 'utf8');
const rules = db.read(FILES.RULES, []).map(r => `${r.topic}: ${r.content}`).join('\n');
const settings = db.read(FILES.SETTINGS, DEFAULT_SETTINGS);
const promptForzado = `=== CONTEXTO DE EMPRESA ===\nEmpresa: ${settings.companyName}\n${ctx}\n\n=== REGLAS ===\n${rules}\n\n=== HISTORIAL RECIENTE CON EL CLIENTE ===\n${userHistory}\n\n=== TU OBJETIVO ===\nRedacta un mensaje natural, amable y persuasivo de seguimiento (follow-up). IMPORTANTE: NO pongas etiquetas ocultas al final ([FUNNEL], [SCORE], etc).`;
const aiResponse = await consultarGroqBlindado([{ role: "system", content: promptForzado }]);
const finalMsg = aiResponse.replace(/(\n\s*\d+\.\s*)*$/g, '').trim();
await sendSafeMessage(jid, finalMsg);
const logEntryOut = { jid: cleanJid(jid), from: "IA (Seguimiento)", msg: finalMsg, date: new Date().toLocaleTimeString('es-DO'), timestamp: Date.now(), type: 'out' };
logs.push(logEntryOut);
db.write(FILES.LOGS, logs.slice(-5000));
io.emit('new_message_update', logEntryOut);
const users = db.read(FILES.USERS, []);
let user = users.find(u => u.id === cleanJid(jid));
if(user) {
user.last_msg = new Date().toLocaleString('es-DO');
user.timestamp = Date.now();
db.write(FILES.USERS, users);
io.emit('user_updated', user);
}
res.json({ success: true });
} catch (e) {
res.json({ success: false, error: e.message });
}
});
app.post('/api/broadcast', async (req, res) => { const { targetStage, message } = req.body; const users = db.read(FILES.USERS, []); const targetUsers = targetStage === 'all' ? users.filter(u => u.id !== 'status@broadcast' && !u.is_admin) : users.filter(u => u.funnel_stage === targetStage && !u.is_admin); if(targetUsers.length === 0) return res.json({ success: false, count: 0 }); res.json({ success: true, count: targetUsers.length }); (async () => { for (let i = 0; i < targetUsers.length; i++) { const u = targetUsers[i]; const delay = Math.floor(Math.random() * (25000 - 10000 + 1)) + 10000; try { await sendSafeMessage(u.id, message); const logs = db.read(FILES.LOGS, []); logs.push({ jid: u.id, from: "MADOKA (Campaña)", msg: message, date: new Date().toLocaleTimeString('es-DO'), timestamp: Date.now(), type: 'out' }); db.write(FILES.LOGS, logs.slice(-5000)); io.emit('new_message_update', logs[logs.length - 1]); } catch (err) {} if (i < targetUsers.length - 1) await new Promise(r => setTimeout(r, delay)); } })(); });
app.post('/api/smart-broadcast', async (req, res) => { const { aiPrompt, message } = req.body; const users = db.read(FILES.USERS, []).filter(u => u.id !== 'status@broadcast' && !u.is_admin); const userList = users.map(u => ({ id: u.id, nombre: u.name, etapa_embudo: u.funnel_stage, ultima_interaccion: new Date(u.timestamp).toISOString(), score_interes: u.score, etiquetas: u.tags })); const systemInstruction = `Actúas como un filtro de base de datos JSON.\nBase de datos actual: ${JSON.stringify(userList)}\nInstrucción: "${aiPrompt}"\nAnaliza y devuelve ÚNICAMENTE un arreglo JSON plano con los "id". Ejemplo: ["1809...1@c.us"]. NO devuelvas texto.`; try { const response = await consultarGroqBlindado([{ role: "user", content: systemInstruction }]); const match = response.match(/\[.*\]/s); if(match) { const targetIds = JSON.parse(match[0]); if(targetIds.length === 0) return res.json({ success: false, error: "Nadie cumple la condición." }); res.json({ success: true, count: targetIds.length }); (async () => { for (let i = 0; i < targetIds.length; i++) { const jid = targetIds[i]; const delay = Math.floor(Math.random() * (25000 - 10000 + 1)) + 10000; try { await sendSafeMessage(jid, message); const logs = db.read(FILES.LOGS, []); logs.push({ jid: jid, from: "MADOKA (Smart IA)", msg: message, date: new Date().toLocaleTimeString('es-DO'), timestamp: Date.now(), type: 'out' }); db.write(FILES.LOGS, logs.slice(-5000)); io.emit('new_message_update', logs[logs.length - 1]); } catch (err) {} if (i < targetIds.length - 1) await new Promise(r => setTimeout(r, delay)); } })(); } else { res.json({ success: false, error: "La IA no devolvió un formato válido."}); } } catch (e) { res.json({ success: false, error: e.message }); } });
app.post('/api/rules', (req, res) => { const rules = db.read(FILES.RULES, []); rules.push({ id: Date.now(), topic: req.body.topic, content: req.body.content }); db.write(FILES.RULES, rules); res.json({ success: true }); });
app.put('/api/rules/:id', (req, res) => { let rules = db.read(FILES.RULES, []); const idx = rules.findIndex(r => r.id == req.params.id); if (idx > -1) { rules[idx].topic = req.body.topic; rules[idx].content = req.body.content; db.write(FILES.RULES, rules); } res.json({ success: true }); });
app.delete('/api/rules/:id', (req, res) => { let rules = db.read(FILES.RULES, []); rules = rules.filter(r => r.id != req.params.id); db.write(FILES.RULES, rules); res.json({ success: true }); });
app.get('/api/commands', (req, res) => res.json(db.read(FILES.COMMANDS, [])));
app.post('/api/commands', (req, res) => { const cmds = db.read(FILES.COMMANDS, []); cmds.push({ id: Date.now(), trigger: req.body.trigger.toLowerCase(), type: req.body.type, content: req.body.content, desc: req.body.desc }); db.write(FILES.COMMANDS, cmds); res.json({ success: true }); });
app.delete('/api/commands/:id', (req, res) => { let cmds = db.read(FILES.COMMANDS, []); cmds = cmds.filter(c => c.id != req.params.id); db.write(FILES.COMMANDS, cmds); res.json({ success: true }); });
app.post('/api/users/toggle', (req, res) => { let users = db.read(FILES.USERS, []); const targetId = cleanJid(req.body.id); const idx = users.findIndex(u => u.id === targetId); if (idx > -1) { users[idx].ia_active = req.body.ia_active; db.write(FILES.USERS, users); io.emit('user_updated', users[idx]); } res.json({ success: true }); });
app.post('/api/users/admin', (req, res) => { let users = db.read(FILES.USERS, []); const targetId = cleanJid(req.body.id); const idx = users.findIndex(u => u.id === targetId); if (idx > -1) { users[idx].is_admin = req.body.is_admin; db.write(FILES.USERS, users); io.emit('user_updated', users[idx]); } res.json({ success: true }); });
app.post('/api/users/toggle-all', (req, res) => {
let users = db.read(FILES.USERS, []);
const newState = req.body.ia_active;
users.forEach(u => {
if (!u.is_admin) u.ia_active = newState;
});
db.write(FILES.USERS, users);
io.emit('all_users_updated');
res.json({ success: true });
});
app.get('/api/reminders', (req, res) => res.json(db.read(FILES.REMINDERS, [])));
app.post('/api/reminders', (req, res) => { const reminders = db.read(FILES.REMINDERS, []); reminders.push({ id: Date.now(), jid: cleanJid(req.body.jid), text: req.body.text, datetime: parseInt(req.body.datetime), type: req.body.type || 'reminder', triggerMsgId: req.body.triggerMsgId || null }); db.write(FILES.REMINDERS, reminders); res.json({ success: true }); });
app.delete('/api/reminders/:id', (req, res) => { let reminders = db.read(FILES.REMINDERS, []); reminders = reminders.filter(r => r.id != req.params.id); db.write(FILES.REMINDERS, reminders); res.json({ success: true }); });
app.get('/qr', (req, res) => {
if (isConnected) return res.send(`