feat: push notifications
Signed-off-by: Omar Sánchez Pizarro <omar.sanchez@pistacero.net>
This commit is contained in:
@@ -8,6 +8,7 @@ import { readFileSync, writeFileSync, existsSync, statSync } from 'fs';
|
||||
import { watch } from 'chokidar';
|
||||
import yaml from 'yaml';
|
||||
import redis from 'redis';
|
||||
import webpush from 'web-push';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -24,6 +25,37 @@ app.use(express.json());
|
||||
// Configuración
|
||||
const CONFIG_PATH = join(PROJECT_ROOT, 'config.yaml');
|
||||
const WORKERS_PATH = join(PROJECT_ROOT, 'workers.json');
|
||||
const PUSH_SUBSCRIPTIONS_PATH = join(PROJECT_ROOT, 'push-subscriptions.json');
|
||||
|
||||
// Inicializar VAPID keys para Web Push
|
||||
let vapidKeys = null;
|
||||
const VAPID_KEYS_PATH = join(PROJECT_ROOT, 'vapid-keys.json');
|
||||
|
||||
function initVAPIDKeys() {
|
||||
try {
|
||||
if (existsSync(VAPID_KEYS_PATH)) {
|
||||
vapidKeys = JSON.parse(readFileSync(VAPID_KEYS_PATH, 'utf8'));
|
||||
console.log('✅ VAPID keys cargadas desde archivo');
|
||||
} else {
|
||||
// Generar nuevas VAPID keys
|
||||
vapidKeys = webpush.generateVAPIDKeys();
|
||||
writeFileSync(VAPID_KEYS_PATH, JSON.stringify(vapidKeys, null, 2), 'utf8');
|
||||
console.log('✅ Nuevas VAPID keys generadas y guardadas');
|
||||
}
|
||||
|
||||
// Configurar web-push con las VAPID keys
|
||||
webpush.setVapidDetails(
|
||||
'mailto:admin@pribyte.cloud', // Contacto (puedes cambiarlo)
|
||||
vapidKeys.publicKey,
|
||||
vapidKeys.privateKey
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error inicializando VAPID keys:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Inicializar VAPID keys al arrancar
|
||||
initVAPIDKeys();
|
||||
|
||||
// Función para obtener la ruta del log (en Docker puede estar en /data/logs)
|
||||
function getLogPath() {
|
||||
@@ -585,6 +617,112 @@ app.get('/api/telegram/threads', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Obtener suscripciones push guardadas
|
||||
function getPushSubscriptions() {
|
||||
return readJSON(PUSH_SUBSCRIPTIONS_PATH, []);
|
||||
}
|
||||
|
||||
// Guardar suscripciones push
|
||||
function savePushSubscriptions(subscriptions) {
|
||||
return writeJSON(PUSH_SUBSCRIPTIONS_PATH, subscriptions);
|
||||
}
|
||||
|
||||
// API Routes para Push Notifications
|
||||
|
||||
// Obtener clave pública VAPID
|
||||
app.get('/api/push/public-key', (req, res) => {
|
||||
if (!vapidKeys || !vapidKeys.publicKey) {
|
||||
return res.status(500).json({ error: 'VAPID keys no están configuradas' });
|
||||
}
|
||||
res.json({ publicKey: vapidKeys.publicKey });
|
||||
});
|
||||
|
||||
// Suscribirse a notificaciones push
|
||||
app.post('/api/push/subscribe', async (req, res) => {
|
||||
try {
|
||||
const subscription = req.body;
|
||||
|
||||
if (!subscription || !subscription.endpoint) {
|
||||
return res.status(400).json({ error: 'Suscripción inválida' });
|
||||
}
|
||||
|
||||
const subscriptions = getPushSubscriptions();
|
||||
|
||||
// Verificar si ya existe esta suscripción
|
||||
const existingIndex = subscriptions.findIndex(
|
||||
sub => sub.endpoint === subscription.endpoint
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
subscriptions[existingIndex] = subscription;
|
||||
} else {
|
||||
subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
savePushSubscriptions(subscriptions);
|
||||
console.log(`✅ Nueva suscripción push guardada. Total: ${subscriptions.length}`);
|
||||
|
||||
res.json({ success: true, totalSubscriptions: subscriptions.length });
|
||||
} catch (error) {
|
||||
console.error('Error guardando suscripción push:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Cancelar suscripción push
|
||||
app.post('/api/push/unsubscribe', async (req, res) => {
|
||||
try {
|
||||
const subscription = req.body;
|
||||
|
||||
if (!subscription || !subscription.endpoint) {
|
||||
return res.status(400).json({ error: 'Suscripción inválida' });
|
||||
}
|
||||
|
||||
const subscriptions = getPushSubscriptions();
|
||||
const filtered = subscriptions.filter(
|
||||
sub => sub.endpoint !== subscription.endpoint
|
||||
);
|
||||
|
||||
savePushSubscriptions(filtered);
|
||||
console.log(`✅ Suscripción push cancelada. Total: ${filtered.length}`);
|
||||
|
||||
res.json({ success: true, totalSubscriptions: filtered.length });
|
||||
} catch (error) {
|
||||
console.error('Error cancelando suscripción push:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Enviar notificación push a todas las suscripciones
|
||||
async function sendPushNotifications(notificationData) {
|
||||
const subscriptions = getPushSubscriptions();
|
||||
|
||||
if (subscriptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify(notificationData);
|
||||
const promises = subscriptions.map(async (subscription) => {
|
||||
try {
|
||||
await webpush.sendNotification(subscription, payload);
|
||||
console.log('✅ Notificación push enviada');
|
||||
} catch (error) {
|
||||
console.error('Error enviando notificación push:', error);
|
||||
|
||||
// Si la suscripción es inválida (404, 410), eliminarla
|
||||
if (error.statusCode === 404 || error.statusCode === 410) {
|
||||
const updatedSubscriptions = getPushSubscriptions().filter(
|
||||
sub => sub.endpoint !== subscription.endpoint
|
||||
);
|
||||
savePushSubscriptions(updatedSubscriptions);
|
||||
console.log(`Suscripción inválida eliminada. Total: ${updatedSubscriptions.length}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
// WebSocket connection
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('Cliente WebSocket conectado');
|
||||
@@ -684,6 +822,21 @@ async function checkForNewArticles() {
|
||||
type: 'new_articles',
|
||||
data: newArticles
|
||||
});
|
||||
|
||||
// Enviar notificaciones push para cada artículo nuevo
|
||||
for (const article of newArticles) {
|
||||
await sendPushNotifications({
|
||||
title: `Nuevo artículo en ${article.platform?.toUpperCase() || 'Wallabicher'}`,
|
||||
body: article.title || 'Artículo nuevo disponible',
|
||||
icon: '/android-chrome-192x192.png',
|
||||
image: article.images?.[0] || null,
|
||||
url: article.url || '/',
|
||||
platform: article.platform,
|
||||
price: article.price,
|
||||
currency: article.currency || '€',
|
||||
id: article.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar el set de claves notificadas
|
||||
|
||||
Reference in New Issue
Block a user