847 lines
28 KiB
JavaScript
847 lines
28 KiB
JavaScript
/**
|
||
* TrasportiNotifications.js
|
||
*
|
||
* Servizio notifiche centralizzato per Trasporti Solidali.
|
||
* USA il telegrambot.js esistente per Telegram, AGGIUNGE Email e Push.
|
||
*
|
||
* NON MODIFICA telegrambot.js - lo importa e usa i suoi metodi.
|
||
*/
|
||
|
||
const nodemailer = require('nodemailer');
|
||
const webpush = require('web-push');
|
||
|
||
// Importa il tuo telegrambot esistente
|
||
const MyTelegramBot = require('../../telegram/telegrambot');
|
||
|
||
// =============================================================================
|
||
// CONFIGURAZIONE
|
||
// =============================================================================
|
||
|
||
const config = {
|
||
// Email SMTP
|
||
smtp: {
|
||
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
||
port: parseInt(process.env.SMTP_PORT) || 465,
|
||
secure: true,
|
||
auth: {
|
||
user: process.env.SMTP_USER,
|
||
pass: process.env.SMTP_PASS
|
||
}
|
||
},
|
||
emailFrom: process.env.SMTP_FROM || 'noreply@trasporti.app',
|
||
|
||
// Push VAPID
|
||
vapidPublicKey: process.env.VAPID_PUBLIC_KEY,
|
||
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY,
|
||
vapidEmail: process.env.VAPID_EMAIL || 'admin@trasporti.app',
|
||
|
||
// App
|
||
appName: process.env.APP_NAME || 'Trasporti Solidali',
|
||
appUrl: process.env.APP_URL || 'https://trasporti.app'
|
||
};
|
||
|
||
// Configura web-push se le chiavi sono presenti
|
||
if (config.vapidPublicKey && config.vapidPrivateKey) {
|
||
webpush.setVapidDetails(
|
||
`mailto:${config.vapidEmail}`,
|
||
config.vapidPublicKey,
|
||
config.vapidPrivateKey
|
||
);
|
||
}
|
||
|
||
// Crea transporter email
|
||
let emailTransporter = null;
|
||
if (config.smtp.auth.user && config.smtp.auth.pass) {
|
||
emailTransporter = nodemailer.createTransport(config.smtp);
|
||
}
|
||
|
||
// =============================================================================
|
||
// TIPI DI NOTIFICA
|
||
// =============================================================================
|
||
|
||
const NotificationType = {
|
||
// Viaggi
|
||
NEW_RIDE_REQUEST: 'new_ride_request',
|
||
REQUEST_ACCEPTED: 'request_accepted',
|
||
REQUEST_REJECTED: 'request_rejected',
|
||
RIDE_REMINDER_24H: 'ride_reminder_24h',
|
||
RIDE_REMINDER_2H: 'ride_reminder_2h',
|
||
RIDE_CANCELLED: 'ride_cancelled',
|
||
RIDE_MODIFIED: 'ride_modified',
|
||
|
||
// Messaggi
|
||
NEW_MESSAGE: 'new_message',
|
||
|
||
// Community
|
||
NEW_COMMUNITY_RIDE: 'new_community_ride',
|
||
|
||
// Sistema
|
||
WEEKLY_DIGEST: 'weekly_digest',
|
||
TEST: 'test',
|
||
WELCOME: 'welcome'
|
||
};
|
||
|
||
// =============================================================================
|
||
// EMOJI PER NOTIFICHE
|
||
// =============================================================================
|
||
|
||
const emo = {
|
||
CAR: '🚗',
|
||
PASSENGER: '🧑🤝🧑',
|
||
CHECK: '✅',
|
||
CROSS: '❌',
|
||
BELL: '🔔',
|
||
CLOCK: '⏰',
|
||
CALENDAR: '📅',
|
||
PIN: '📍',
|
||
ARROW: '➡️',
|
||
MESSAGE: '💬',
|
||
STAR: '⭐',
|
||
WARNING: '⚠️',
|
||
INFO: 'ℹ️',
|
||
WAVE: '👋',
|
||
HEART: '❤️'
|
||
};
|
||
|
||
// =============================================================================
|
||
// TRADUZIONI NOTIFICHE
|
||
// =============================================================================
|
||
|
||
const translations = {
|
||
it: {
|
||
// Richieste
|
||
NEW_RIDE_REQUEST_TITLE: 'Nuova richiesta di passaggio',
|
||
NEW_RIDE_REQUEST_BODY: '{{passengerName}} chiede un passaggio per il viaggio {{departure}} → {{destination}} del {{date}}',
|
||
NEW_RIDE_REQUEST_ACTION: 'Visualizza richiesta',
|
||
|
||
// Accettazione
|
||
REQUEST_ACCEPTED_TITLE: 'Richiesta accettata!',
|
||
REQUEST_ACCEPTED_BODY: '{{driverName}} ha accettato la tua richiesta per {{departure}} → {{destination}} del {{date}}',
|
||
REQUEST_ACCEPTED_ACTION: 'Visualizza viaggio',
|
||
|
||
// Rifiuto
|
||
REQUEST_REJECTED_TITLE: 'Richiesta non accettata',
|
||
REQUEST_REJECTED_BODY: '{{driverName}} non ha potuto accettare la tua richiesta per {{departure}} → {{destination}}',
|
||
|
||
// Promemoria
|
||
RIDE_REMINDER_24H_TITLE: 'Viaggio domani!',
|
||
RIDE_REMINDER_24H_BODY: 'Promemoria: domani hai un viaggio {{departure}} → {{destination}} alle {{time}}',
|
||
RIDE_REMINDER_2H_TITLE: 'Viaggio tra 2 ore!',
|
||
RIDE_REMINDER_2H_BODY: 'Il tuo viaggio {{departure}} → {{destination}} parte tra 2 ore alle {{time}}',
|
||
|
||
// Cancellazione
|
||
RIDE_CANCELLED_TITLE: 'Viaggio cancellato',
|
||
RIDE_CANCELLED_BODY: 'Il viaggio {{departure}} → {{destination}} del {{date}} è stato cancellato',
|
||
RIDE_CANCELLED_REASON: 'Motivo: {{reason}}',
|
||
|
||
// Modifica
|
||
RIDE_MODIFIED_TITLE: 'Viaggio modificato',
|
||
RIDE_MODIFIED_BODY: 'Il viaggio {{departure}} → {{destination}} è stato modificato. Verifica i nuovi dettagli.',
|
||
|
||
// Messaggi
|
||
NEW_MESSAGE_TITLE: 'Nuovo messaggio',
|
||
NEW_MESSAGE_BODY: '{{senderName}}: {{preview}}',
|
||
|
||
// Community
|
||
NEW_COMMUNITY_RIDE_TITLE: 'Nuovo viaggio nella tua zona',
|
||
NEW_COMMUNITY_RIDE_BODY: 'Nuovo viaggio disponibile: {{departure}} → {{destination}} il {{date}}',
|
||
|
||
// Test
|
||
TEST_TITLE: 'Notifica di test',
|
||
TEST_BODY: 'Questa è una notifica di test da Trasporti Solidali. Se la vedi, tutto funziona!',
|
||
|
||
// Welcome
|
||
WELCOME_TITLE: 'Benvenuto su Trasporti Solidali!',
|
||
WELCOME_BODY: 'Le notifiche sono state attivate correttamente. Riceverai aggiornamenti sui tuoi viaggi.',
|
||
|
||
// Common
|
||
VIEW_DETAILS: 'Visualizza dettagli',
|
||
REPLY: 'Rispondi'
|
||
},
|
||
|
||
en: {
|
||
NEW_RIDE_REQUEST_TITLE: 'New ride request',
|
||
NEW_RIDE_REQUEST_BODY: '{{passengerName}} requests a ride for {{departure}} → {{destination}} on {{date}}',
|
||
NEW_RIDE_REQUEST_ACTION: 'View request',
|
||
|
||
REQUEST_ACCEPTED_TITLE: 'Request accepted!',
|
||
REQUEST_ACCEPTED_BODY: '{{driverName}} accepted your request for {{departure}} → {{destination}} on {{date}}',
|
||
REQUEST_ACCEPTED_ACTION: 'View ride',
|
||
|
||
REQUEST_REJECTED_TITLE: 'Request not accepted',
|
||
REQUEST_REJECTED_BODY: '{{driverName}} could not accept your request for {{departure}} → {{destination}}',
|
||
|
||
RIDE_REMINDER_24H_TITLE: 'Ride tomorrow!',
|
||
RIDE_REMINDER_24H_BODY: 'Reminder: tomorrow you have a ride {{departure}} → {{destination}} at {{time}}',
|
||
RIDE_REMINDER_2H_TITLE: 'Ride in 2 hours!',
|
||
RIDE_REMINDER_2H_BODY: 'Your ride {{departure}} → {{destination}} leaves in 2 hours at {{time}}',
|
||
|
||
RIDE_CANCELLED_TITLE: 'Ride cancelled',
|
||
RIDE_CANCELLED_BODY: 'The ride {{departure}} → {{destination}} on {{date}} has been cancelled',
|
||
RIDE_CANCELLED_REASON: 'Reason: {{reason}}',
|
||
|
||
RIDE_MODIFIED_TITLE: 'Ride modified',
|
||
RIDE_MODIFIED_BODY: 'The ride {{departure}} → {{destination}} has been modified. Check the new details.',
|
||
|
||
NEW_MESSAGE_TITLE: 'New message',
|
||
NEW_MESSAGE_BODY: '{{senderName}}: {{preview}}',
|
||
|
||
NEW_COMMUNITY_RIDE_TITLE: 'New ride in your area',
|
||
NEW_COMMUNITY_RIDE_BODY: 'New ride available: {{departure}} → {{destination}} on {{date}}',
|
||
|
||
TEST_TITLE: 'Test notification',
|
||
TEST_BODY: 'This is a test notification from Trasporti Solidali. If you see this, everything works!',
|
||
|
||
WELCOME_TITLE: 'Welcome to Trasporti Solidali!',
|
||
WELCOME_BODY: 'Notifications have been enabled successfully. You will receive updates about your rides.',
|
||
|
||
VIEW_DETAILS: 'View details',
|
||
REPLY: 'Reply'
|
||
}
|
||
};
|
||
|
||
// =============================================================================
|
||
// HELPER FUNCTIONS
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Ottiene traduzione con sostituzione variabili
|
||
*/
|
||
function getTranslation(lang, key, data = {}) {
|
||
const langTranslations = translations[lang] || translations['it'];
|
||
let text = langTranslations[key] || translations['it'][key] || key;
|
||
|
||
// Sostituisci {{variabile}}
|
||
Object.keys(data).forEach(varName => {
|
||
const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g');
|
||
text = text.replace(regex, data[varName] || '');
|
||
});
|
||
|
||
return text;
|
||
}
|
||
|
||
/**
|
||
* Mappa tipo notifica a chiave preferenze
|
||
*/
|
||
function getPreferenceKey(type) {
|
||
const map = {
|
||
[NotificationType.NEW_RIDE_REQUEST]: 'newRideRequest',
|
||
[NotificationType.REQUEST_ACCEPTED]: 'requestAccepted',
|
||
[NotificationType.REQUEST_REJECTED]: 'requestRejected',
|
||
[NotificationType.RIDE_REMINDER_24H]: 'rideReminder24h',
|
||
[NotificationType.RIDE_REMINDER_2H]: 'rideReminder2h',
|
||
[NotificationType.RIDE_CANCELLED]: 'rideCancelled',
|
||
[NotificationType.RIDE_MODIFIED]: 'rideCancelled',
|
||
[NotificationType.NEW_MESSAGE]: 'newMessage',
|
||
[NotificationType.NEW_COMMUNITY_RIDE]: 'newCommunityRide',
|
||
[NotificationType.WEEKLY_DIGEST]: 'weeklyDigest',
|
||
[NotificationType.TEST]: null, // Sempre inviato
|
||
[NotificationType.WELCOME]: null // Sempre inviato
|
||
};
|
||
return map[type];
|
||
}
|
||
|
||
/**
|
||
* Verifica se inviare notifica su un canale
|
||
*/
|
||
function shouldSend(prefs, channel, type) {
|
||
if (!prefs) return false;
|
||
|
||
const channelPrefs = prefs[channel];
|
||
if (!channelPrefs || !channelPrefs.enabled) return false;
|
||
|
||
// Test e Welcome sempre inviati se canale abilitato
|
||
if (type === NotificationType.TEST || type === NotificationType.WELCOME) {
|
||
return true;
|
||
}
|
||
|
||
const prefKey = getPreferenceKey(type);
|
||
if (!prefKey) return true; // Se non mappato, invia
|
||
|
||
return channelPrefs[prefKey] !== false; // Default true
|
||
}
|
||
|
||
/**
|
||
* Tronca testo
|
||
*/
|
||
function truncate(text, maxLength = 100) {
|
||
if (!text || text.length <= maxLength) return text;
|
||
return text.substring(0, maxLength - 3) + '...';
|
||
}
|
||
|
||
// =============================================================================
|
||
// EMAIL TEMPLATES
|
||
// =============================================================================
|
||
|
||
function buildEmailHtml(type, data, lang = 'it') {
|
||
const t = (key) => getTranslation(lang, key, data);
|
||
|
||
const title = t(`${type.toUpperCase()}_TITLE`);
|
||
const body = t(`${type.toUpperCase()}_BODY`);
|
||
|
||
// Colori per tipo
|
||
const colors = {
|
||
[NotificationType.NEW_RIDE_REQUEST]: '#667eea',
|
||
[NotificationType.REQUEST_ACCEPTED]: '#21ba45',
|
||
[NotificationType.REQUEST_REJECTED]: '#c10015',
|
||
[NotificationType.RIDE_REMINDER_24H]: '#f2711c',
|
||
[NotificationType.RIDE_REMINDER_2H]: '#db2828',
|
||
[NotificationType.RIDE_CANCELLED]: '#c10015',
|
||
[NotificationType.NEW_MESSAGE]: '#2185d0',
|
||
[NotificationType.NEW_COMMUNITY_RIDE]: '#a333c8',
|
||
[NotificationType.TEST]: '#667eea',
|
||
[NotificationType.WELCOME]: '#21ba45'
|
||
};
|
||
|
||
const color = colors[type] || '#667eea';
|
||
|
||
// Emoji per tipo
|
||
const emojis = {
|
||
[NotificationType.NEW_RIDE_REQUEST]: emo.PASSENGER,
|
||
[NotificationType.REQUEST_ACCEPTED]: emo.CHECK,
|
||
[NotificationType.REQUEST_REJECTED]: emo.CROSS,
|
||
[NotificationType.RIDE_REMINDER_24H]: emo.CALENDAR,
|
||
[NotificationType.RIDE_REMINDER_2H]: emo.CLOCK,
|
||
[NotificationType.RIDE_CANCELLED]: emo.WARNING,
|
||
[NotificationType.NEW_MESSAGE]: emo.MESSAGE,
|
||
[NotificationType.NEW_COMMUNITY_RIDE]: emo.CAR,
|
||
[NotificationType.TEST]: emo.BELL,
|
||
[NotificationType.WELCOME]: emo.WAVE
|
||
};
|
||
|
||
const emoji = emojis[type] || emo.BELL;
|
||
|
||
// CTA button
|
||
let ctaHtml = '';
|
||
if (data.actionUrl) {
|
||
const actionText = data.actionText || t('VIEW_DETAILS');
|
||
ctaHtml = `
|
||
<div style="text-align: center; margin: 30px 0;">
|
||
<a href="${data.actionUrl}"
|
||
style="display: inline-block; padding: 14px 32px; background: ${color};
|
||
color: white; text-decoration: none; border-radius: 8px;
|
||
font-weight: 600; font-size: 16px;">
|
||
${actionText}
|
||
</a>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Info viaggio
|
||
let rideInfoHtml = '';
|
||
if (data.departure && data.destination) {
|
||
rideInfoHtml = `
|
||
<div style="background: #f8f9fa; border-radius: 12px; padding: 20px; margin: 20px 0;">
|
||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
|
||
<span style="font-size: 24px;">${emo.PIN}</span>
|
||
<div>
|
||
<div style="color: #666; font-size: 12px;">Partenza</div>
|
||
<div style="font-weight: 600; font-size: 16px;">${data.departure}</div>
|
||
</div>
|
||
</div>
|
||
<div style="text-align: center; color: #999; margin: 10px 0;">${emo.ARROW}</div>
|
||
<div style="display: flex; align-items: center; gap: 10px;">
|
||
<span style="font-size: 24px;">${emo.PIN}</span>
|
||
<div>
|
||
<div style="color: #666; font-size: 12px;">Destinazione</div>
|
||
<div style="font-weight: 600; font-size: 16px;">${data.destination}</div>
|
||
</div>
|
||
</div>
|
||
${data.date ? `
|
||
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
||
<span style="color: #666;">${emo.CALENDAR} ${data.date}</span>
|
||
${data.time ? `<span style="margin-left: 15px; color: #666;">${emo.CLOCK} ${data.time}</span>` : ''}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
</head>
|
||
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5;">
|
||
<div style="max-width: 600px; margin: 0 auto; background: white;">
|
||
<!-- Header -->
|
||
<div style="background: linear-gradient(135deg, ${color} 0%, ${color}dd 100%); padding: 30px 20px; text-align: center;">
|
||
<div style="font-size: 48px; margin-bottom: 10px;">${emoji}</div>
|
||
<h1 style="margin: 0; color: white; font-size: 24px; font-weight: 600;">${title}</h1>
|
||
</div>
|
||
|
||
<!-- Content -->
|
||
<div style="padding: 30px 20px;">
|
||
<p style="font-size: 16px; line-height: 1.6; color: #333; margin: 0 0 20px;">
|
||
${body}
|
||
</p>
|
||
|
||
${rideInfoHtml}
|
||
|
||
${data.reason ? `<p style="color: #666; font-style: italic;">${t('RIDE_CANCELLED_REASON')}</p>` : ''}
|
||
|
||
${ctaHtml}
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div style="background: #f8f9fa; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0;">
|
||
<p style="margin: 0 0 10px; color: #666; font-size: 14px;">
|
||
${config.appName}
|
||
</p>
|
||
<p style="margin: 0; color: #999; font-size: 12px;">
|
||
Ricevi questa email perché hai attivato le notifiche.
|
||
<a href="${config.appUrl}/impostazioni" style="color: ${color};">Gestisci preferenze</a>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`;
|
||
}
|
||
|
||
// =============================================================================
|
||
// TELEGRAM MESSAGE BUILDER
|
||
// =============================================================================
|
||
|
||
function buildTelegramMessage(type, data, lang = 'it') {
|
||
const t = (key) => getTranslation(lang, key, data);
|
||
|
||
const title = t(`${type.toUpperCase()}_TITLE`);
|
||
const body = t(`${type.toUpperCase()}_BODY`);
|
||
|
||
// Emoji per tipo
|
||
const emojis = {
|
||
[NotificationType.NEW_RIDE_REQUEST]: emo.PASSENGER,
|
||
[NotificationType.REQUEST_ACCEPTED]: emo.CHECK,
|
||
[NotificationType.REQUEST_REJECTED]: emo.CROSS,
|
||
[NotificationType.RIDE_REMINDER_24H]: emo.CALENDAR,
|
||
[NotificationType.RIDE_REMINDER_2H]: emo.CLOCK,
|
||
[NotificationType.RIDE_CANCELLED]: emo.WARNING,
|
||
[NotificationType.NEW_MESSAGE]: emo.MESSAGE,
|
||
[NotificationType.NEW_COMMUNITY_RIDE]: emo.CAR,
|
||
[NotificationType.TEST]: emo.BELL,
|
||
[NotificationType.WELCOME]: emo.WAVE
|
||
};
|
||
|
||
const emoji = emojis[type] || emo.BELL;
|
||
|
||
let message = `${emoji} <b>${title}</b>\n\n${body}`;
|
||
|
||
// Aggiungi info viaggio
|
||
if (data.departure && data.destination) {
|
||
message += `\n\n${emo.PIN} <b>Percorso:</b>\n${data.departure} ${emo.ARROW} ${data.destination}`;
|
||
if (data.date) {
|
||
message += `\n${emo.CALENDAR} ${data.date}`;
|
||
}
|
||
if (data.time) {
|
||
message += ` ${emo.CLOCK} ${data.time}`;
|
||
}
|
||
}
|
||
|
||
// Motivo cancellazione
|
||
if (data.reason) {
|
||
message += `\n\n<i>${t('RIDE_CANCELLED_REASON')}</i>`;
|
||
}
|
||
|
||
return message;
|
||
}
|
||
|
||
// =============================================================================
|
||
// PUSH NOTIFICATION BUILDER
|
||
// =============================================================================
|
||
|
||
function buildPushPayload(type, data, lang = 'it') {
|
||
const t = (key) => getTranslation(lang, key, data);
|
||
|
||
const title = t(`${type.toUpperCase()}_TITLE`);
|
||
let body = t(`${type.toUpperCase()}_BODY`);
|
||
|
||
// Tronca body per push
|
||
body = truncate(body, 150);
|
||
|
||
// Icone per tipo
|
||
const icons = {
|
||
[NotificationType.NEW_RIDE_REQUEST]: '/icons/request.png',
|
||
[NotificationType.REQUEST_ACCEPTED]: '/icons/accepted.png',
|
||
[NotificationType.REQUEST_REJECTED]: '/icons/rejected.png',
|
||
[NotificationType.RIDE_REMINDER_24H]: '/icons/reminder.png',
|
||
[NotificationType.RIDE_REMINDER_2H]: '/icons/urgent.png',
|
||
[NotificationType.RIDE_CANCELLED]: '/icons/cancelled.png',
|
||
[NotificationType.NEW_MESSAGE]: '/icons/message.png',
|
||
[NotificationType.NEW_COMMUNITY_RIDE]: '/icons/community.png',
|
||
[NotificationType.TEST]: '/icons/notification.png',
|
||
[NotificationType.WELCOME]: '/icons/welcome.png'
|
||
};
|
||
|
||
return {
|
||
title,
|
||
body,
|
||
icon: icons[type] || '/icons/notification.png',
|
||
badge: '/icons/badge.png',
|
||
tag: type,
|
||
data: {
|
||
type,
|
||
url: data.actionUrl || config.appUrl,
|
||
...data
|
||
},
|
||
actions: data.actionUrl ? [
|
||
{ action: 'open', title: t('VIEW_DETAILS') }
|
||
] : []
|
||
};
|
||
}
|
||
|
||
// =============================================================================
|
||
// MAIN SERVICE
|
||
// =============================================================================
|
||
|
||
const TrasportiNotifications = {
|
||
|
||
// Esponi tipi e emoji
|
||
NotificationType,
|
||
emo,
|
||
|
||
// Esponi config
|
||
config,
|
||
|
||
/**
|
||
* Invia notifica su tutti i canali abilitati
|
||
*
|
||
* @param {Object} user - Utente destinatario (con notificationPreferences)
|
||
* @param {string} type - Tipo notifica (da NotificationType)
|
||
* @param {Object} data - Dati per template
|
||
* @param {string} idapp - ID app (per Telegram)
|
||
* @returns {Object} { success, results: { email, telegram, push } }
|
||
*/
|
||
async sendNotification(user, type, data, idapp) {
|
||
const results = {
|
||
email: null,
|
||
telegram: null,
|
||
push: null
|
||
};
|
||
|
||
const prefs = user.notificationPreferences || {};
|
||
const lang = user.lang || 'it';
|
||
|
||
// Aggiungi URL azione se non presente
|
||
if (!data.actionUrl && data.rideId) {
|
||
data.actionUrl = `${config.appUrl}/trasporti/viaggio/${data.rideId}`;
|
||
}
|
||
if (!data.actionUrl && data.requestId) {
|
||
data.actionUrl = `${config.appUrl}/trasporti/richieste/${data.requestId}`;
|
||
}
|
||
if (!data.actionUrl && data.chatId) {
|
||
data.actionUrl = `${config.appUrl}/trasporti/chat/${data.chatId}`;
|
||
}
|
||
|
||
// EMAIL
|
||
if (shouldSend(prefs, 'email', type) && user.email) {
|
||
results.email = await this.sendEmail(user.email, type, data, lang);
|
||
}
|
||
|
||
// TELEGRAM (usa il tuo telegrambot.js esistente!)
|
||
const telegId = user.profile?.teleg_id || prefs.telegram?.chatId;
|
||
if (shouldSend(prefs, 'telegram', type) && telegId) {
|
||
results.telegram = await this.sendTelegram(idapp, telegId, type, data, lang);
|
||
}
|
||
|
||
// PUSH
|
||
const pushSub = prefs.push?.subscription;
|
||
if (shouldSend(prefs, 'push', type) && pushSub) {
|
||
results.push = await this.sendPush(pushSub, type, data, lang);
|
||
}
|
||
|
||
return {
|
||
success: Object.values(results).some(r => r?.success),
|
||
results
|
||
};
|
||
},
|
||
|
||
/**
|
||
* Invia notifica a multipli utenti
|
||
*/
|
||
async sendNotificationToMany(users, type, data, idapp) {
|
||
const results = [];
|
||
|
||
for (const user of users) {
|
||
try {
|
||
const result = await this.sendNotification(user, type, data, idapp);
|
||
results.push({ userId: user._id, ...result });
|
||
|
||
// Delay per evitare rate limiting
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
} catch (error) {
|
||
results.push({ userId: user._id, success: false, error: error.message });
|
||
}
|
||
}
|
||
|
||
return results;
|
||
},
|
||
|
||
// ===========================================================================
|
||
// EMAIL
|
||
// ===========================================================================
|
||
|
||
async sendEmail(to, type, data, lang = 'it') {
|
||
if (!emailTransporter) {
|
||
return { success: false, error: 'Email not configured' };
|
||
}
|
||
|
||
try {
|
||
const t = (key) => getTranslation(lang, key, data);
|
||
const subject = `${config.appName} - ${t(`${type.toUpperCase()}_TITLE`)}`;
|
||
const html = buildEmailHtml(type, data, lang);
|
||
|
||
const info = await emailTransporter.sendMail({
|
||
from: `"${config.appName}" <${config.emailFrom}>`,
|
||
to,
|
||
subject,
|
||
html
|
||
});
|
||
|
||
return { success: true, messageId: info.messageId };
|
||
} catch (error) {
|
||
console.error('Email send error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
},
|
||
|
||
// ===========================================================================
|
||
// TELEGRAM (usa il tuo MyTelegramBot!)
|
||
// ===========================================================================
|
||
|
||
async sendTelegram(idapp, chatId, type, data, lang = 'it') {
|
||
try {
|
||
const message = buildTelegramMessage(type, data, lang);
|
||
|
||
// USA IL TUO METODO ESISTENTE!
|
||
const result = await MyTelegramBot.local_sendMsgTelegramByIdTelegram(
|
||
idapp,
|
||
chatId,
|
||
message,
|
||
null, // message_id
|
||
null, // chat_id reply
|
||
false, // ripr_menuPrec
|
||
null, // MyForm (bottoni)
|
||
'' // img
|
||
);
|
||
|
||
return { success: true, messageId: result?.message_id };
|
||
} catch (error) {
|
||
console.error('Telegram send error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Invia notifica Telegram con bottoni inline
|
||
*/
|
||
async sendTelegramWithButtons(idapp, chatId, type, data, buttons, lang = 'it') {
|
||
try {
|
||
const message = buildTelegramMessage(type, data, lang);
|
||
|
||
// Crea inline keyboard
|
||
const cl = MyTelegramBot.getclTelegByidapp(idapp);
|
||
if (!cl) {
|
||
return { success: false, error: 'Telegram client not found' };
|
||
}
|
||
|
||
const keyboard = cl.getInlineKeyboard(lang, buttons);
|
||
|
||
const result = await MyTelegramBot.local_sendMsgTelegramByIdTelegram(
|
||
idapp,
|
||
chatId,
|
||
message,
|
||
null,
|
||
null,
|
||
false,
|
||
keyboard,
|
||
''
|
||
);
|
||
|
||
return { success: true, messageId: result?.message_id };
|
||
} catch (error) {
|
||
console.error('Telegram send error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
},
|
||
|
||
// ===========================================================================
|
||
// PUSH
|
||
// ===========================================================================
|
||
|
||
async sendPush(subscription, type, data, lang = 'it') {
|
||
if (!config.vapidPublicKey || !config.vapidPrivateKey) {
|
||
return { success: false, error: 'Push not configured' };
|
||
}
|
||
|
||
try {
|
||
const payload = JSON.stringify(buildPushPayload(type, data, lang));
|
||
|
||
await webpush.sendNotification(subscription, payload);
|
||
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('Push send error:', error);
|
||
|
||
// Subscription scaduta
|
||
if (error.statusCode === 410 || error.statusCode === 404) {
|
||
return { success: false, error: 'Subscription expired', expired: true };
|
||
}
|
||
|
||
return { success: false, error: error.message };
|
||
}
|
||
},
|
||
|
||
// ===========================================================================
|
||
// METODI SPECIFICI PER TRASPORTI
|
||
// ===========================================================================
|
||
|
||
/**
|
||
* Notifica nuova richiesta passaggio al conducente
|
||
*/
|
||
async notifyNewRideRequest(driver, passenger, ride, request, idapp) {
|
||
return this.sendNotification(driver, NotificationType.NEW_RIDE_REQUEST, {
|
||
passengerName: `${passenger.name} ${passenger.surname}`,
|
||
departure: ride.departure?.city || ride.departure?.address,
|
||
destination: ride.destination?.city || ride.destination?.address,
|
||
date: formatDate(ride.departureTime),
|
||
time: formatTime(ride.departureTime),
|
||
seats: request.seats || 1,
|
||
rideId: ride._id,
|
||
requestId: request._id,
|
||
actionUrl: `${config.appUrl}/trasporti/richieste/${request._id}`
|
||
}, idapp);
|
||
},
|
||
|
||
/**
|
||
* Notifica richiesta accettata al passeggero
|
||
*/
|
||
async notifyRequestAccepted(passenger, driver, ride, idapp) {
|
||
return this.sendNotification(passenger, NotificationType.REQUEST_ACCEPTED, {
|
||
driverName: `${driver.name} ${driver.surname}`,
|
||
departure: ride.departure?.city || ride.departure?.address,
|
||
destination: ride.destination?.city || ride.destination?.address,
|
||
date: formatDate(ride.departureTime),
|
||
time: formatTime(ride.departureTime),
|
||
rideId: ride._id,
|
||
actionUrl: `${config.appUrl}/trasporti/viaggio/${ride._id}`
|
||
}, idapp);
|
||
},
|
||
|
||
/**
|
||
* Notifica richiesta rifiutata al passeggero
|
||
*/
|
||
async notifyRequestRejected(passenger, driver, ride, reason, idapp) {
|
||
return this.sendNotification(passenger, NotificationType.REQUEST_REJECTED, {
|
||
driverName: `${driver.name} ${driver.surname}`,
|
||
departure: ride.departure?.city || ride.departure?.address,
|
||
destination: ride.destination?.city || ride.destination?.address,
|
||
reason
|
||
}, idapp);
|
||
},
|
||
|
||
/**
|
||
* Notifica promemoria viaggio
|
||
*/
|
||
async notifyRideReminder(user, ride, hoursBefor, idapp) {
|
||
const type = hoursBefor === 24
|
||
? NotificationType.RIDE_REMINDER_24H
|
||
: NotificationType.RIDE_REMINDER_2H;
|
||
|
||
return this.sendNotification(user, type, {
|
||
departure: ride.departure?.city || ride.departure?.address,
|
||
destination: ride.destination?.city || ride.destination?.address,
|
||
date: formatDate(ride.departureTime),
|
||
time: formatTime(ride.departureTime),
|
||
rideId: ride._id,
|
||
actionUrl: `${config.appUrl}/trasporti/viaggio/${ride._id}`
|
||
}, idapp);
|
||
},
|
||
|
||
/**
|
||
* Notifica viaggio cancellato
|
||
*/
|
||
async notifyRideCancelled(user, ride, reason, idapp) {
|
||
return this.sendNotification(user, NotificationType.RIDE_CANCELLED, {
|
||
departure: ride.departure?.city || ride.departure?.address,
|
||
destination: ride.destination?.city || ride.destination?.address,
|
||
date: formatDate(ride.departureTime),
|
||
reason,
|
||
rideId: ride._id
|
||
}, idapp);
|
||
},
|
||
|
||
/**
|
||
* Notifica nuovo messaggio
|
||
*/
|
||
async notifyNewMessage(recipient, sender, message, chatId, idapp) {
|
||
return this.sendNotification(recipient, NotificationType.NEW_MESSAGE, {
|
||
senderName: `${sender.name} ${sender.surname}`,
|
||
preview: truncate(message.text, 100),
|
||
chatId,
|
||
actionUrl: `${config.appUrl}/trasporti/chat/${chatId}`
|
||
}, idapp);
|
||
},
|
||
|
||
/**
|
||
* Invia notifica di test
|
||
*/
|
||
async sendTestNotification(user, channel, idapp) {
|
||
const type = NotificationType.TEST;
|
||
const data = {
|
||
actionUrl: `${config.appUrl}/trasporti/impostazioni`
|
||
};
|
||
const lang = user.lang || 'it';
|
||
|
||
if (channel === 'email' && user.email) {
|
||
return this.sendEmail(user.email, type, data, lang);
|
||
}
|
||
|
||
const telegId = user.profile?.teleg_id || user.notificationPreferences?.telegram?.chatId;
|
||
if (channel === 'telegram' && telegId) {
|
||
return this.sendTelegram(idapp, telegId, type, data, lang);
|
||
}
|
||
|
||
const pushSub = user.notificationPreferences?.push?.subscription;
|
||
if (channel === 'push' && pushSub) {
|
||
return this.sendPush(pushSub, type, data, lang);
|
||
}
|
||
|
||
if (channel === 'all') {
|
||
return this.sendNotification(user, type, data, idapp);
|
||
}
|
||
|
||
return { success: false, error: 'Invalid channel or not configured' };
|
||
}
|
||
};
|
||
|
||
// =============================================================================
|
||
// HELPER DATE
|
||
// =============================================================================
|
||
|
||
function formatDate(date) {
|
||
if (!date) return '';
|
||
const d = new Date(date);
|
||
return d.toLocaleDateString('it-IT', {
|
||
weekday: 'short',
|
||
day: 'numeric',
|
||
month: 'short'
|
||
});
|
||
}
|
||
|
||
function formatTime(date) {
|
||
if (!date) return '';
|
||
const d = new Date(date);
|
||
return d.toLocaleTimeString('it-IT', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
// =============================================================================
|
||
// EXPORT
|
||
// =============================================================================
|
||
|
||
module.exports = TrasportiNotifications; |