- Aggiornamento Viaggi

This commit is contained in:
Surya Paolo
2025-12-30 11:36:42 +01:00
parent 85141df8a4
commit fb40743694
46 changed files with 3676 additions and 2104 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,4 +1,4 @@
const Chat = require('../models/Chat');
const Chat = require('../models/viaggi/Chat');
const Message = require('../models/Message');
const { User } = require('../models/user');

View File

@@ -1,7 +1,7 @@
const mongoose = require('mongoose');
const Feedback = require('../models/Feedback');
const Ride = require('../models/Ride');
const RideRequest = require('../models/RideRequest');
const Feedback = require('../models/viaggi/Feedback');
const Ride = require('../models/viaggi/Ride');
const RideRequest = require('../models/viaggi/RideRequest');
const { User } = require('../models/user');
// ============================================================
@@ -338,7 +338,7 @@ const getUserFeedback = async (req, res) => {
const [feedbacks, total, stats] = await Promise.all([
Feedback.find(query)
.populate('fromUserId', 'username name surname profile.img')
.populate('rideId', 'departure destination dateTime')
.populate('rideId', 'departure destination departureDate')
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit)),
@@ -598,7 +598,7 @@ const canLeaveFeedback = async (req, res) => {
_id: ride._id,
departure: ride.departure,
destination: ride.destination,
dateTime: ride.dateTime,
departureDate: ride.departureDate,
},
},
});
@@ -823,7 +823,7 @@ const getMyGivenFeedback = async (req, res) => {
const [feedbacks, total] = await Promise.all([
Feedback.find({ idapp, fromUserId: userId })
.populate('toUserId', 'username name surname profile.img')
.populate('rideId', 'departure destination dateTime')
.populate('rideId', 'departure destination departureDate')
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit)),
@@ -872,7 +872,7 @@ const getMyReceivedFeedback = async (req, res) => {
const [feedbacks, total, stats] = await Promise.all([
Feedback.find({ idapp, toUserId: userId })
.populate('fromUserId', 'username name surname profile.img')
.populate('rideId', 'departure destination dateTime')
.populate('rideId', 'departure destination departureDate')
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit)),

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,11 @@
const mongoose = require('mongoose');
const RideRequest = require('../models/RideRequest');
const Ride = require('../models/Ride');
const Chat = require('../models/Chat');
const RideRequest = require('../models/viaggi/RideRequest');
const Ride = require('../models/viaggi/Ride');
const Chat = require('../models/viaggi/Chat');
const Message = require('../models/Message');
const TrasportiNotifications = require('./viaggi/TrasportiNotifications'); // Aggiungi import
/**
* Helper per convertire ID in ObjectId
*/
@@ -126,12 +128,12 @@ const createRequest = async (req, res) => {
// Popola i dati per la risposta
await rideRequest.populate('passengerId', 'username name surname profile.img');
await rideRequest.populate('rideId', 'departure destination dateTime');
await rideRequest.populate('rideId', 'departure destination departureDate');
// Crea o recupera la chat tra passeggero e conducente
let chat;
let chatId = null;
try {
chat = await Chat.findOrCreateDirect(idapp, passengerId, ride.userId, rideId);
chatId = chat._id;
@@ -183,6 +185,116 @@ const createRequest = async (req, res) => {
}
};
/**
* Crea richiesta passaggio da community ride
*/
const createRequestFromRide = async (req, res) => {
try {
const { rideId } = req.params;
const passengerId = req.user._id;
const { seats = 1, message, pickupPoint, dropoffPoint } = req.body;
// Verifica che il viaggio esista
const ride = await Ride.findById(rideId);
if (!ride) {
return res.status(404).json({
success: false,
message: 'Viaggio non trovato',
});
}
// Non puoi richiedere il tuo stesso viaggio
if (ride.userId.toString() === passengerId.toString()) {
return res.status(400).json({
success: false,
message: 'Non puoi richiedere il tuo stesso viaggio',
});
}
// Verifica che ci siano posti disponibili
if (ride.passengers.available < seats) {
return res.status(400).json({
success: false,
message: 'Non ci sono abbastanza posti disponibili',
});
}
// Verifica che non ci sia già una richiesta
const existingRequest = await RideRequest.findOne({
rideId,
passengerId,
status: { $in: ['pending', 'accepted'] },
});
if (existingRequest) {
return res.status(400).json({
success: false,
message: 'Hai già una richiesta attiva per questo viaggio',
});
}
// Crea la richiesta
const request = new RideRequest({
rideId,
passengerId,
driverId: ride.userId,
seats,
message,
pickupPoint: pickupPoint || ride.departure,
dropoffPoint: dropoffPoint || ride.destination,
status: 'pending',
});
await request.save();
// Popola i dati per la risposta
await request.populate([
{ path: 'passengerId', select: 'name surname profile.img' },
{ path: 'driverId', select: 'name surname profile.img' },
{ path: 'rideId', select: 'departure destination departureDate' },
]);
// ============================================================
// NOTIFICA AL CONDUCENTE
// ============================================================
try {
// Ottieni dati completi del driver (per email, telegram, push)
const driver = await User.findById(ride.userId).select(
'name surname email lang profile notificationPreferences idapp'
);
// Ottieni dati passeggero
const passenger = req.user; // o await User.findById(passengerId)
// Invia notifica su tutti i canali abilitati
await TrasportiNotifications.notifyNewRideRequest(
driver, // destinatario
passenger, // chi ha fatto la richiesta
ride, // il viaggio
request, // la richiesta creata
driver.idapp, // idapp per Telegram
);
} catch (notifError) {
// Log errore ma non bloccare la risposta
console.error('Errore invio notifica:', notifError);
}
return res.status(201).json({
success: true,
message: 'Richiesta inviata con successo',
data: request,
});
} catch (error) {
console.error('Error creating ride request:', error);
return res.status(500).json({
success: false,
message: 'Errore nella creazione della richiesta',
error: error.message,
});
}
};
/**
* @desc Ottieni le richieste per un viaggio (per il conducente)
* @route GET /api/viaggi/requests/ride/:rideId
@@ -327,7 +439,7 @@ const getPendingRequests = async (req, res) => {
status: 'pending',
})
.populate('passengerId', 'username name surname profile.img profile.driverProfile.averageRating')
.populate('rideId', 'departure destination dateTime passengers')
.populate('rideId', 'departure destination departureDate passengers')
.sort({ createdAt: -1 });
res.json({
@@ -415,7 +527,7 @@ const acceptRequest = async (req, res) => {
if (!ride.confirmedPassengers) {
ride.confirmedPassengers = [];
}
ride.confirmedPassengers.push({
userId: request.passengerId,
seats: request.seatsRequested,
@@ -429,10 +541,7 @@ const acceptRequest = async (req, res) => {
await ride.updateAvailableSeats();
} else {
// Fallback manuale
const totalConfirmed = ride.confirmedPassengers.reduce(
(sum, p) => sum + (p.seats || 1),
0
);
const totalConfirmed = ride.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0);
if (ride.passengers) {
ride.passengers.available = Math.max(0, (ride.passengers.total || 0) - totalConfirmed);
}
@@ -462,7 +571,7 @@ const acceptRequest = async (req, res) => {
timestamp: new Date(),
type: 'ride_accepted',
};
// Incrementa unread per il passeggero
if (!chat.unreadCount) {
chat.unreadCount = new Map();
@@ -471,7 +580,7 @@ const acceptRequest = async (req, res) => {
const current = chat.unreadCount.get(passengerIdStr) || 0;
chat.unreadCount.set(passengerIdStr, current + 1);
chat.markModified('unreadCount');
await chat.save();
} catch (chatError) {
console.error('Errore invio messaggio chat:', chatError);
@@ -480,7 +589,7 @@ const acceptRequest = async (req, res) => {
// TODO: Inviare notifica push al passeggero
await request.populate('passengerId', 'username name surname profile.img');
await request.populate('rideId', 'departure destination dateTime');
await request.populate('rideId', 'departure destination departureDate');
res.json({
success: true,
@@ -570,7 +679,7 @@ const rejectRequest = async (req, res) => {
timestamp: new Date(),
type: 'ride_rejected',
};
if (!chat.unreadCount) {
chat.unreadCount = new Map();
}
@@ -578,7 +687,7 @@ const rejectRequest = async (req, res) => {
const current = chat.unreadCount.get(passengerIdStr) || 0;
chat.unreadCount.set(passengerIdStr, current + 1);
chat.markModified('unreadCount');
await chat.save();
} catch (chatError) {
console.error('Errore invio messaggio chat:', chatError);
@@ -647,14 +756,11 @@ const cancelRequest = async (req, res) => {
ride.confirmedPassengers = ride.confirmedPassengers.filter(
(p) => p.userId.toString() !== request.passengerId.toString()
);
if (typeof ride.updateAvailableSeats === 'function') {
await ride.updateAvailableSeats();
} else {
const totalConfirmed = ride.confirmedPassengers.reduce(
(sum, p) => sum + (p.seats || 1),
0
);
const totalConfirmed = ride.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0);
if (ride.passengers) {
ride.passengers.available = Math.max(0, (ride.passengers.total || 0) - totalConfirmed);
}
@@ -718,7 +824,7 @@ const getRequestById = async (req, res) => {
// Verifica che l'utente sia coinvolto
const passengerId = request.passengerId?._id || request.passengerId;
const driverId = request.driverId?._id || request.driverId;
const isPassenger = passengerId?.toString() === userId.toString();
const isDriver = driverId?.toString() === userId.toString();
@@ -768,7 +874,7 @@ const getReceivedRequests = async (req, res) => {
// Fetch requests
const requests = await RideRequest.find(query)
.populate('passengerId', 'username name surname profile.img profile.driverProfile.averageRating')
.populate('rideId', 'departure destination dateTime passengers')
.populate('rideId', 'departure destination departureDate passengers')
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit))
@@ -858,7 +964,7 @@ const getSentRequests = async (req, res) => {
// Fetch requests
const requests = await RideRequest.find(query)
.populate('driverId', 'username name surname profile.img profile.driverProfile.averageRating')
.populate('rideId', 'departure destination dateTime passengers')
.populate('rideId', 'departure destination departureDate passengers')
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit))
@@ -872,7 +978,7 @@ const getSentRequests = async (req, res) => {
enriched.rideInfo = {
departure: enriched.rideId.departure?.city || enriched.rideId.departure,
destination: enriched.rideId.destination?.city || enriched.rideId.destination,
dateTime: enriched.rideId.dateTime,
departureDate: enriched.rideId.departureDate,
availableSeats: enriched.rideId.passengers?.available || 0,
};
}
@@ -949,4 +1055,5 @@ module.exports = {
getRequestById,
getReceivedRequests,
getSentRequests,
};
createRequestFromRide,
};

View File

@@ -0,0 +1,847 @@
/**
* 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;

View File

@@ -0,0 +1,422 @@
// ============================================================
// 🔧 SETTINGS CONTROLLER - Trasporti Solidali
// ============================================================
// File: server/controllers/viaggi/settingsController.js
const UserSettings = require('../../models/viaggi/UserSettings');
/**
* 📄 GET /api/viaggi/settings
* Ottieni le impostazioni dell'utente
*/
exports.getSettings = async (req, res) => {
try {
const { idapp } = req.query;
const userId = req.user._id;
// Validazione
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è richiesto'
});
}
// Ottieni o crea impostazioni
const settings = await UserSettings.getOrCreateSettings(idapp, userId);
return res.status(200).json({
success: true,
data: settings.toClientJSON()
});
} catch (error) {
console.error('❌ Errore getSettings:', error);
return res.status(500).json({
success: false,
message: 'Errore nel caricamento delle impostazioni',
error: error.message
});
}
};
/**
* 📝 PUT /api/viaggi/settings
* Aggiorna le impostazioni dell'utente
*/
exports.updateSettings = async (req, res) => {
try {
const userId = req.user._id;
const idapp = req.user.idapp;
const updates = req.body;
// Validazione
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è richiesto'
});
}
if (!updates || Object.keys(updates).length === 0) {
return res.status(400).json({
success: false,
message: 'Nessuna modifica specificata'
});
}
// Aggiorna impostazioni
const settings = await UserSettings.updateSettings(idapp, userId, updates);
return res.status(200).json({
success: true,
message: 'Impostazioni aggiornate con successo',
data: settings.toClientJSON()
});
} catch (error) {
console.error('❌ Errore updateSettings:', error);
return res.status(500).json({
success: false,
message: 'Errore nell\'aggiornamento delle impostazioni',
error: error.message
});
}
};
/**
* 📝 PATCH /api/viaggi/settings/notifications
* Aggiorna solo le impostazioni notifiche
*/
exports.updateNotifications = async (req, res) => {
try {
const idapp = req.user.idapp;
const userId = req.user._id;
const { notifications } = req.body;
// Validazione
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è richiesto'
});
}
if (!notifications) {
return res.status(400).json({
success: false,
message: 'notifications è richiesto'
});
}
// Aggiorna solo notifiche
const settings = await UserSettings.updateSettings(idapp, userId, { notifications });
return res.status(200).json({
success: true,
message: 'Notifiche aggiornate',
data: settings.notifications
});
} catch (error) {
console.error('❌ Errore updateNotifications:', error);
return res.status(500).json({
success: false,
message: 'Errore nell\'aggiornamento delle notifiche',
error: error.message
});
}
};
/**
* 📝 PATCH /api/viaggi/settings/privacy
* Aggiorna solo le impostazioni privacy
*/
exports.updatePrivacy = async (req, res) => {
try {
const idapp = req.user.idapp;
const userId = req.user._id;
const { privacy } = req.body;
// Validazione
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è richiesto'
});
}
if (!privacy) {
return res.status(400).json({
success: false,
message: 'privacy è richiesto'
});
}
// Aggiorna solo privacy
const settings = await UserSettings.updateSettings(idapp, userId, { privacy });
return res.status(200).json({
success: true,
message: 'Privacy aggiornata',
data: settings.privacy
});
} catch (error) {
console.error('❌ Errore updatePrivacy:', error);
return res.status(500).json({
success: false,
message: 'Errore nell\'aggiornamento della privacy',
error: error.message
});
}
};
/**
* 📝 PATCH /api/viaggi/settings/ride-preferences
* Aggiorna solo le preferenze viaggi
*/
exports.updateRidePreferences = async (req, res) => {
try {
const idapp = req.user.idapp;
const userId = req.user._id;
const { ridePreferences } = req.body;
// Validazione
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è richiesto'
});
}
if (!ridePreferences) {
return res.status(400).json({
success: false,
message: 'ridePreferences è richiesto'
});
}
// Aggiorna preferenze viaggi
const settings = await UserSettings.updateSettings(idapp, userId, { ridePreferences });
return res.status(200).json({
success: true,
message: 'Preferenze viaggi aggiornate',
data: settings.ridePreferences
});
} catch (error) {
console.error('❌ Errore updateRidePreferences:', error);
return res.status(500).json({
success: false,
message: 'Errore nell\'aggiornamento delle preferenze',
error: error.message
});
}
};
/**
* 📝 PATCH /api/viaggi/settings/interface
* Aggiorna solo le impostazioni interfaccia
*/
exports.updateInterface = async (req, res) => {
try {
const idapp = req.user.idapp;
const userId = req.user._id;
const { interface: interfaceSettings } = req.body;
// Validazione
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è richiesto'
});
}
if (!interfaceSettings) {
return res.status(400).json({
success: false,
message: 'interface è richiesto'
});
}
// Aggiorna interfaccia
const settings = await UserSettings.updateSettings(idapp, userId, {
interface: interfaceSettings
});
return res.status(200).json({
success: true,
message: 'Interfaccia aggiornata',
data: settings.interface
});
} catch (error) {
console.error('❌ Errore updateInterface:', error);
return res.status(500).json({
success: false,
message: 'Errore nell\'aggiornamento dell\'interfaccia',
error: error.message
});
}
};
/**
* 🔄 POST /api/viaggi/settings/reset
* Reset impostazioni ai valori predefiniti
*/
exports.resetSettings = async (req, res) => {
try {
const idapp = req.user.idapp;
const userId = req.user._id;
const { section } = req.body; // Opzionale: resetta solo una sezione
// Validazione
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è richiesto'
});
}
// Trova impostazioni esistenti
let settings = await UserSettings.findOne({ idapp, userId });
if (!settings) {
return res.status(404).json({
success: false,
message: 'Impostazioni non trovate'
});
}
if (section) {
// Reset solo di una sezione specifica
const schema = UserSettings.schema.paths[section];
if (!schema) {
return res.status(400).json({
success: false,
message: 'Sezione non valida'
});
}
// Ottieni valori predefiniti dalla schema
settings[section] = schema.defaultValue || {};
} else {
// Reset completo - cancella e ricrea
await UserSettings.deleteOne({ idapp, userId });
settings = await UserSettings.getOrCreateSettings(idapp, userId);
}
await settings.save();
return res.status(200).json({
success: true,
message: section
? `Sezione ${section} resettata`
: 'Impostazioni resettate ai valori predefiniti',
data: settings.toClientJSON()
});
} catch (error) {
console.error('❌ Errore resetSettings:', error);
return res.status(500).json({
success: false,
message: 'Errore nel reset delle impostazioni',
error: error.message
});
}
};
/**
* 📊 GET /api/viaggi/settings/export
* Esporta tutte le impostazioni (per backup o trasferimento)
*/
exports.exportSettings = async (req, res) => {
try {
const { idapp } = req.query;
const userId = req.user._id;
// Validazione
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è richiesto'
});
}
const settings = await UserSettings.findOne({ idapp, userId });
if (!settings) {
return res.status(404).json({
success: false,
message: 'Impostazioni non trovate'
});
}
// Esporta in formato JSON pulito
const exportData = {
exportDate: new Date().toISOString(),
userId: userId.toString(),
idapp,
settings: settings.toClientJSON()
};
return res.status(200).json({
success: true,
data: exportData
});
} catch (error) {
console.error('❌ Errore exportSettings:', error);
return res.status(500).json({
success: false,
message: 'Errore nell\'esportazione delle impostazioni',
error: error.message
});
}
};
/**
* 📥 POST /api/viaggi/settings/import
* Importa impostazioni da backup
*/
exports.importSettings = async (req, res) => {
try {
const idapp = req.user.idapp;
const userId = req.user._id;
const { settings: importedSettings } = req.body;
// Validazione
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è richiesto'
});
}
if (!importedSettings) {
return res.status(400).json({
success: false,
message: 'settings è richiesto'
});
}
// Aggiorna con le impostazioni importate
const settings = await UserSettings.updateSettings(idapp, userId, importedSettings);
return res.status(200).json({
success: true,
message: 'Impostazioni importate con successo',
data: settings.toClientJSON()
});
} catch (error) {
console.error('❌ Errore importSettings:', error);
return res.status(500).json({
success: false,
message: 'Errore nell\'importazione delle impostazioni',
error: error.message
});
}
};

View File

@@ -0,0 +1,506 @@
/**
* trasportiNotificationsController.js
*
* Controller API per gestire le preferenze di notifica utente.
* Funziona insieme a TrasportiNotifications.js
*/
const mongoose = require('mongoose');
const TrasportiNotifications = require('./TrasportiNotifications');
// =============================================================================
// SCHEMA PREFERENZE (da aggiungere al model User)
// =============================================================================
const notificationPreferencesSchema = new mongoose.Schema({
email: {
enabled: { type: Boolean, default: true },
newRideRequest: { type: Boolean, default: true },
requestAccepted: { type: Boolean, default: true },
requestRejected: { type: Boolean, default: true },
rideReminder24h: { type: Boolean, default: true },
rideReminder2h: { type: Boolean, default: true },
rideCancelled: { type: Boolean, default: true },
newMessage: { type: Boolean, default: true },
newCommunityRide: { type: Boolean, default: false },
weeklyDigest: { type: Boolean, default: false }
},
telegram: {
enabled: { type: Boolean, default: false },
chatId: { type: Number, default: 0 },
username: { type: String, default: '' },
connectedAt: { type: Date },
newRideRequest: { type: Boolean, default: true },
requestAccepted: { type: Boolean, default: true },
requestRejected: { type: Boolean, default: true },
rideReminder24h: { type: Boolean, default: true },
rideReminder2h: { type: Boolean, default: true },
rideCancelled: { type: Boolean, default: true },
newMessage: { type: Boolean, default: true }
},
push: {
enabled: { type: Boolean, default: false },
subscription: { type: mongoose.Schema.Types.Mixed },
subscribedAt: { type: Date },
newRideRequest: { type: Boolean, default: true },
requestAccepted: { type: Boolean, default: true },
requestRejected: { type: Boolean, default: true },
rideReminder24h: { type: Boolean, default: true },
rideReminder2h: { type: Boolean, default: true },
rideCancelled: { type: Boolean, default: true },
newMessage: { type: Boolean, default: true }
}
}, { _id: false });
// =============================================================================
// STORAGE CODICI TELEGRAM (in-memory, usa Redis in produzione)
// =============================================================================
const telegramConnectCodes = new Map();
// Pulizia codici scaduti ogni 5 minuti
setInterval(() => {
const now = Date.now();
for (const [code, data] of telegramConnectCodes) {
if (now - data.createdAt > 10 * 60 * 1000) { // 10 minuti
telegramConnectCodes.delete(code);
}
}
}, 5 * 60 * 1000);
/**
* Genera codice random 6 caratteri
*/
function generateCode() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Escludo caratteri ambigui
let code = '';
for (let i = 0; i < 6; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
}
// =============================================================================
// CONTROLLER
// =============================================================================
const trasportiNotificationsController = {
// Esponi schema per User model
notificationPreferencesSchema,
/**
* GET /api/trasporti/notifications/preferences
* Ottiene preferenze notifiche utente
*/
async getNotificationPreferences(req, res) {
try {
const user = req.user;
// Default preferences se non esistono
const defaultPrefs = {
email: {
enabled: true,
newRideRequest: true,
requestAccepted: true,
requestRejected: true,
rideReminder24h: true,
rideReminder2h: true,
rideCancelled: true,
newMessage: true,
newCommunityRide: false,
weeklyDigest: false
},
telegram: {
enabled: false,
chatId: user.profile?.teleg_id || 0,
username: user.profile?.teleg_username || '',
newRideRequest: true,
requestAccepted: true,
requestRejected: true,
rideReminder24h: true,
rideReminder2h: true,
rideCancelled: true,
newMessage: true
},
push: {
enabled: false,
newRideRequest: true,
requestAccepted: true,
requestRejected: true,
rideReminder24h: true,
rideReminder2h: true,
rideCancelled: true,
newMessage: true
}
};
// Merge con preferenze salvate
const prefs = user.notificationPreferences || {};
const mergedPrefs = {
email: { ...defaultPrefs.email, ...prefs.email },
telegram: { ...defaultPrefs.telegram, ...prefs.telegram },
push: { ...defaultPrefs.push, ...prefs.push }
};
// Sync chatId da profile se presente
if (user.profile?.teleg_id && !mergedPrefs.telegram.chatId) {
mergedPrefs.telegram.chatId = user.profile.teleg_id;
mergedPrefs.telegram.enabled = true;
}
res.json({
success: true,
data: {
email: user.email,
preferences: mergedPrefs,
vapidPublicKey: TrasportiNotifications.config.vapidPublicKey,
telegramBotUsername: process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot'
}
});
} catch (error) {
console.error('getNotificationPreferences error:', error);
res.status(500).json({ success: false, message: error.message });
}
},
/**
* PUT /api/trasporti/notifications/preferences
* Aggiorna preferenze notifiche
*/
async updateNotificationPreferences(req, res) {
try {
const { User } = require('../../models/user');
const { email, telegram, push } = req.body;
const updateData = {};
// Email preferences
if (email) {
Object.keys(email).forEach(key => {
if (key !== 'enabled' || typeof email[key] === 'boolean') {
updateData[`notificationPreferences.email.${key}`] = email[key];
}
});
}
// Telegram preferences (escludi chatId, username - gestiti via connect)
if (telegram) {
const allowedKeys = ['enabled', 'newRideRequest', 'requestAccepted', 'requestRejected',
'rideReminder24h', 'rideReminder2h', 'rideCancelled', 'newMessage'];
Object.keys(telegram).forEach(key => {
if (allowedKeys.includes(key)) {
updateData[`notificationPreferences.telegram.${key}`] = telegram[key];
}
});
}
// Push preferences (escludi subscription - gestito via subscribe)
if (push) {
const allowedKeys = ['enabled', 'newRideRequest', 'requestAccepted', 'requestRejected',
'rideReminder24h', 'rideReminder2h', 'rideCancelled', 'newMessage'];
Object.keys(push).forEach(key => {
if (allowedKeys.includes(key)) {
updateData[`notificationPreferences.push.${key}`] = push[key];
}
});
}
await User.updateOne(
{ _id: req.user._id },
{ $set: updateData }
);
res.json({ success: true, message: 'Preferenze aggiornate' });
} catch (error) {
console.error('updateNotificationPreferences error:', error);
res.status(500).json({ success: false, message: error.message });
}
},
/**
* POST /api/trasporti/notifications/telegram/code
* Genera codice per connessione Telegram
*/
async generateTelegramCode(req, res) {
try {
const userId = req.user._id.toString();
// Rimuovi codici esistenti per questo utente
for (const [code, data] of telegramConnectCodes) {
if (data.userId === userId) {
telegramConnectCodes.delete(code);
}
}
// Genera nuovo codice
let code;
do {
code = generateCode();
} while (telegramConnectCodes.has(code));
// Salva
telegramConnectCodes.set(code, {
userId,
createdAt: Date.now(),
chatId: null,
username: null
});
res.json({
success: true,
data: {
code,
expiresIn: 600, // 10 minuti
botUsername: process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot',
instructions: `Invia "${code}" al bot @${process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot'} su Telegram`
}
});
} catch (error) {
console.error('generateTelegramCode error:', error);
res.status(500).json({ success: false, message: error.message });
}
},
/**
* POST /api/trasporti/notifications/telegram/connect
* Completa connessione Telegram dopo validazione codice dal bot
*/
async connectTelegram(req, res) {
try {
const { User } = require('../../models/user');
const { code } = req.body;
if (!code) {
return res.status(400).json({ success: false, message: 'Codice richiesto' });
}
const codeData = telegramConnectCodes.get(code.toUpperCase());
if (!codeData) {
return res.status(400).json({ success: false, message: 'Codice non valido o scaduto' });
}
if (codeData.userId !== req.user._id.toString()) {
return res.status(400).json({ success: false, message: 'Codice non valido' });
}
// Verifica che il bot abbia validato il codice (impostando chatId)
if (!codeData.chatId) {
return res.status(400).json({
success: false,
message: 'Invia prima il codice al bot su Telegram',
needsBotInteraction: true
});
}
// Aggiorna utente
await User.updateOne(
{ _id: req.user._id },
{
$set: {
'notificationPreferences.telegram.enabled': true,
'notificationPreferences.telegram.chatId': codeData.chatId,
'notificationPreferences.telegram.username': codeData.username || '',
'notificationPreferences.telegram.connectedAt': new Date(),
// Retrocompatibilità con profile.teleg_id
'profile.teleg_id': codeData.chatId,
'profile.teleg_username': codeData.username || ''
}
}
);
// Rimuovi codice usato
telegramConnectCodes.delete(code.toUpperCase());
// Invia messaggio benvenuto
const idapp = req.user.idapp;
await TrasportiNotifications.sendTelegram(
idapp,
codeData.chatId,
TrasportiNotifications.NotificationType.WELCOME,
{},
req.user.lang || 'it'
);
res.json({
success: true,
message: 'Telegram connesso!',
data: {
chatId: codeData.chatId,
username: codeData.username
}
});
} catch (error) {
console.error('connectTelegram error:', error);
res.status(500).json({ success: false, message: error.message });
}
},
/**
* DELETE /api/trasporti/notifications/telegram/disconnect
* Disconnette Telegram
*/
async disconnectTelegram(req, res) {
try {
const { User } = require('../../models/user');
// Ottieni chatId prima di disconnettere per inviare messaggio
const chatId = req.user.notificationPreferences?.telegram?.chatId || req.user.profile?.teleg_id;
const idapp = req.user.idapp;
// Aggiorna utente
await User.updateOne(
{ _id: req.user._id },
{
$set: {
'notificationPreferences.telegram.enabled': false,
'notificationPreferences.telegram.chatId': 0,
'notificationPreferences.telegram.username': '',
'notificationPreferences.telegram.connectedAt': null,
'profile.teleg_id': 0,
'profile.teleg_username': ''
}
}
);
// Invia messaggio di disconnessione
if (chatId && idapp) {
const MyTelegramBot = require('./telegram/telegrambot');
await MyTelegramBot.local_sendMsgTelegramByIdTelegram(
idapp,
chatId,
'👋 Telegram disconnesso da Trasporti Solidali.\n\nPuoi riconnettere in qualsiasi momento dalla pagina impostazioni.',
null, null, false, null, ''
);
}
res.json({ success: true, message: 'Telegram disconnesso' });
} catch (error) {
console.error('disconnectTelegram error:', error);
res.status(500).json({ success: false, message: error.message });
}
},
/**
* POST /api/trasporti/notifications/push/subscribe
* Registra subscription push
*/
async subscribePushNotifications(req, res) {
try {
const { User } = require('../../models/user');
const { subscription } = req.body;
if (!subscription || !subscription.endpoint) {
return res.status(400).json({ success: false, message: 'Subscription non valida' });
}
await User.updateOne(
{ _id: req.user._id },
{
$set: {
'notificationPreferences.push.enabled': true,
'notificationPreferences.push.subscription': subscription,
'notificationPreferences.push.subscribedAt': new Date()
}
}
);
res.json({ success: true, message: 'Push notifications attivate' });
} catch (error) {
console.error('subscribePushNotifications error:', error);
res.status(500).json({ success: false, message: error.message });
}
},
/**
* DELETE /api/trasporti/notifications/push/unsubscribe
* Rimuove subscription push
*/
async unsubscribePushNotifications(req, res) {
try {
const { User } = require('../../models/user');
await User.updateOne(
{ _id: req.user._id },
{
$set: {
'notificationPreferences.push.enabled': false,
'notificationPreferences.push.subscription': null
}
}
);
res.json({ success: true, message: 'Push notifications disattivate' });
} catch (error) {
console.error('unsubscribePushNotifications error:', error);
res.status(500).json({ success: false, message: error.message });
}
},
/**
* POST /api/trasporti/notifications/test
* Invia notifica di test
*/
async sendTestNotification(req, res) {
try {
const { channel } = req.body; // 'email', 'telegram', 'push', 'all'
const idapp = req.user.idapp;
const result = await TrasportiNotifications.sendTestNotification(req.user, channel, idapp);
if (result.success) {
res.json({ success: true, message: `Notifica di test inviata su ${channel}` });
} else {
res.status(400).json({ success: false, message: result.error });
}
} catch (error) {
console.error('sendTestNotification error:', error);
res.status(500).json({ success: false, message: error.message });
}
},
/**
* Handler per il bot Telegram quando riceve un codice
* Chiamare questa funzione dal tuo telegrambot.js
*/
handleTelegramCodeFromBot(code, chatId, username) {
const codeUpper = code.toUpperCase();
const codeData = telegramConnectCodes.get(codeUpper);
if (!codeData) {
return { success: false, error: 'Codice non valido o scaduto' };
}
// Aggiorna con chatId e username
codeData.chatId = chatId;
codeData.username = username;
telegramConnectCodes.set(codeUpper, codeData);
return { success: true, userId: codeData.userId };
},
/**
* Rimuovi subscription push scadute
* Chiamare quando si riceve errore 410/404
*/
async removePushSubscription(userId) {
try {
const { User } = require('../../models/user');
await User.updateOne(
{ _id: userId },
{
$set: {
'notificationPreferences.push.subscription': null,
'notificationPreferences.push.enabled': false
}
}
);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}
};
module.exports = trasportiNotificationsController;

View File

@@ -0,0 +1,219 @@
// ============================================================
// 📊 WIDGET & STATS CONTROLLER - Trasporti Solidali
// ============================================================
// File: server/controllers/viaggi/widgetController.js
const Ride = require('../../models/viaggi/Ride');
const RideRequest = require('../../models/viaggi/RideRequest');
const Feedback = require('../../models/viaggi/Feedback');
const Chat = require('../../models/viaggi/Chat');
const mongoose = require('mongoose');
/**
* 📊 GET /api/viaggi/widget/data
* Ottieni dati per il widget dashboard
*/
exports.getWidgetData = async (req, res) => {
try {
const { idapp } = req.query;
const userId = req.user._id;
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è richiesto'
});
}
const now = new Date();
// Query parallele per ottimizzare
const [
offersCount,
requestsCount,
recentRides,
myActiveRides,
pendingRequestsCount,
unreadMessagesCount
] = await Promise.all([
// Conta offerte attive
Ride.countDocuments({
idapp,
type: 'offer',
status: 'active',
departureDate: { $gte: now }
}),
// Conta richieste attive
Ride.countDocuments({
idapp,
type: 'request',
status: 'active',
departureDate: { $gte: now }
}),
// Ultimi viaggi pubblicati (non propri)
Ride.find({
idapp,
userId: { $ne: userId },
status: 'active',
departureDate: { $gte: now }
})
.sort({ createdAt: -1 })
.limit(5)
.populate('userId', 'name surname profile')
.lean(),
// I miei viaggi attivi
Ride.find({
idapp,
userId: userId,
status: 'active',
departureDate: { $gte: now }
})
.sort({ departureDate: 1 })
.limit(3)
.lean(),
// Richieste pendenti ricevute (per i miei viaggi)
RideRequest.countDocuments({
idapp,
driverUserId: userId,
status: 'pending'
}),
// Messaggi non letti
Chat.countDocuments({
idapp,
participants: userId,
isDeleted: false,
[`deletedBy.${userId}`]: { $ne: true },
'messages': {
$elemMatch: {
senderId: { $ne: userId },
readBy: { $ne: userId }
}
}
})
]);
// Calcola "matches" - viaggi compatibili con le mie richieste
let matchesCount = 0;
const myRequests = await Ride.find({
idapp,
userId: userId,
type: 'request',
status: 'active',
departureDate: { $gte: now }
}).select('departure destination departureDate').lean();
if (myRequests.length > 0) {
// Per ogni mia richiesta, cerca offerte compatibili
for (const request of myRequests) {
const compatibleOffers = await Ride.countDocuments({
idapp,
userId: { $ne: userId },
type: 'offer',
status: 'active',
departureDate: {
$gte: new Date(request.departureDate.getTime() - 2 * 60 * 60 * 1000), // -2h
$lte: new Date(request.departureDate.getTime() + 2 * 60 * 60 * 1000) // +2h
},
// Potresti aggiungere filtri geografici qui
'departure.city': request.departure?.city,
'destination.city': request.destination?.city
});
matchesCount += compatibleOffers;
}
}
return res.status(200).json({
success: true,
data: {
stats: {
offers: offersCount,
requests: requestsCount,
matches: matchesCount
},
recentRides: recentRides,
myActiveRides: myActiveRides,
pendingRequests: pendingRequestsCount,
unreadMessages: unreadMessagesCount
}
});
} catch (error) {
console.error('❌ Errore getWidgetData:', error);
return res.status(500).json({
success: false,
message: 'Errore nel caricamento dei dati widget',
error: error.message
});
}
};
/**
* 📊 GET /api/viaggi/stats/quick
* Statistiche rapide per badge/notifiche
*/
exports.getQuickStats = async (req, res) => {
try {
const { idapp } = req.query;
const userId = req.user._id;
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è richiesto'
});
}
const [pendingRequests, unreadMessages, activeRides] = await Promise.all([
RideRequest.countDocuments({
idapp,
driverUserId: userId,
status: 'pending'
}),
Chat.countDocuments({
idapp,
participants: userId,
isDeleted: false,
[`deletedBy.${userId}`]: { $ne: true },
'messages': {
$elemMatch: {
senderId: { $ne: userId },
readBy: { $ne: userId }
}
}
}),
Ride.countDocuments({
idapp,
userId: userId,
status: 'active',
departureDate: { $gte: new Date() }
})
]);
return res.status(200).json({
success: true,
data: {
pendingRequests,
unreadMessages,
activeRides,
totalNotifications: pendingRequests + unreadMessages
}
});
} catch (error) {
console.error('❌ Errore getQuickStats:', error);
return res.status(500).json({
success: false,
message: 'Errore nel caricamento delle statistiche rapide',
error: error.message
});
}
};
module.exports = exports;

View File

@@ -0,0 +1,77 @@
// Helper per calcolare le date dalle ricorrenze
const getRecurrenceDates = (ride, startRange, endRange) => {
const { recurrence, departureDate } = ride;
if (!recurrence || recurrence.type === 'once') {
return [new Date(departureDate)];
}
const dates = [];
const start = new Date(startRange || recurrence.startDate || departureDate);
const end = new Date(endRange || recurrence.endDate || new Date(start.getTime() + 365 * 24 * 60 * 60 * 1000)); // Default 1 anno
const excludedDatesSet = new Set(
(recurrence.excludedDates || []).map(d => new Date(d).toISOString().split('T')[0])
);
switch (recurrence.type) {
case 'weekly':
if (!recurrence.daysOfWeek || recurrence.daysOfWeek.length === 0) break;
let current = new Date(start);
while (current <= end) {
const dayOfWeek = current.getDay();
if (recurrence.daysOfWeek.includes(dayOfWeek)) {
const dateStr = current.toISOString().split('T')[0];
if (!excludedDatesSet.has(dateStr)) {
dates.push(new Date(current));
}
}
current.setDate(current.getDate() + 1);
}
break;
case 'custom_days':
if (!recurrence.daysOfWeek || recurrence.daysOfWeek.length === 0) break;
let curr = new Date(start);
while (curr <= end) {
const dayOfWeek = curr.getDay();
if (recurrence.daysOfWeek.includes(dayOfWeek)) {
const dateStr = curr.toISOString().split('T')[0];
if (!excludedDatesSet.has(dateStr)) {
dates.push(new Date(curr));
}
}
curr.setDate(curr.getDate() + 1);
}
break;
case 'custom_dates':
if (!recurrence.customDates || recurrence.customDates.length === 0) break;
recurrence.customDates.forEach(date => {
const d = new Date(date);
if (d >= start && d <= end) {
const dateStr = d.toISOString().split('T')[0];
if (!excludedDatesSet.has(dateStr)) {
dates.push(d);
}
}
});
break;
}
return dates.length > 0 ? dates : [new Date(departureDate)];
};
const isRideActiveOnDate = (ride, targetDate) => {
const dates = getRecurrenceDates(ride, targetDate, targetDate);
const targetStr = new Date(targetDate).toISOString().split('T')[0];
return dates.some(d => d.toISOString().split('T')[0] === targetStr);
};
module.exports = {
getRecurrenceDates,
isRideActiveOnDate
};

View File

@@ -32,6 +32,8 @@ const i18n = require('i18n');
const shared_consts = require('../tools/shared_nodejs');
const { notificationPreferencesSchema } = require('../controllers/viaggi/trasportiNotificationsController');
mongoose.Promise = global.Promise;
mongoose.level = 'F';
@@ -847,6 +849,10 @@ const UserSchema = new mongoose.Schema(
},
},
},
notificationPreferences: {
type: notificationPreferencesSchema,
default: () => ({}),
},
updatedAt: { type: Date, default: Date.now },
},
{
@@ -7072,7 +7078,9 @@ UserSchema.statics.addNewSite = async function (idappPass, body) {
}
if (arrSite && arrSite.length === 1 && numutenti < 2) {
const MyTelegramBot = require('../telegram/telegrambot');
//const MyTelegramBot = require('../telegram/telegrambot');
const MyTelegramBot = require('../telegram');
// Nessun Sito Installato e Nessun Utente installato !
let myuser = new User();

View File

@@ -339,7 +339,7 @@ FeedbackSchema.statics.getRatingDistribution = async function (idapp, userId, ro
// Hook post-save per aggiornare rating utente
FeedbackSchema.post('save', async function (doc) {
try {
const { User } = require('./User');
const { User } = require('../User');
const stats = await mongoose.model('Feedback').getStatsForUser(doc.idapp, doc.toUserId);

View File

@@ -134,7 +134,6 @@ const VehicleSchema = new Schema(
{
type: {
type: String,
enum: ['auto', 'moto', 'furgone', 'minibus', 'altro'],
default: 'auto',
},
brand: {
@@ -309,7 +308,7 @@ const RideSchema = new Schema(
required: true,
},
waypoints: [WaypointSchema],
dateTime: {
departureDate: {
type: Date,
required: true,
index: true,
@@ -422,8 +421,8 @@ RideSchema.index({ 'departure.city': 1, 'destination.city': 1 });
RideSchema.index({ 'departure.coordinates': '2dsphere' });
RideSchema.index({ 'destination.coordinates': '2dsphere' });
RideSchema.index({ 'waypoints.location.city': 1 });
RideSchema.index({ dateTime: 1, status: 1 });
RideSchema.index({ idapp: 1, status: 1, dateTime: 1 });
RideSchema.index({ departureDate: 1, status: 1 });
RideSchema.index({ idapp: 1, status: 1, departureDate: 1 });
// Virtual per verificare se il viaggio è pieno
RideSchema.virtual('isFull').get(function () {
@@ -490,7 +489,7 @@ RideSchema.statics.findActiveByCity = function (idapp, departureCity, destinatio
const query = {
idapp,
status: { $in: ['active', 'full'] },
dateTime: { $gte: new Date() },
departureDate: { $gte: new Date() },
};
if (departureCity) {
@@ -507,12 +506,12 @@ RideSchema.statics.findActiveByCity = function (idapp, departureCity, destinatio
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(options.date);
endOfDay.setHours(23, 59, 59, 999);
query.dateTime = { $gte: startOfDay, $lte: endOfDay };
query.departureDate = { $gte: startOfDay, $lte: endOfDay };
}
return this.find(query)
.populate('userId', 'username name surname profile.driverProfile.averageRating')
.sort({ dateTime: 1 });
.sort({ departureDate: 1 });
};
// Ricerca viaggi che passano per una città intermedia
@@ -521,7 +520,7 @@ RideSchema.statics.findPassingThrough = function (idapp, cityName, options = {})
const query = {
idapp,
status: { $in: ['active'] },
dateTime: { $gte: new Date() },
departureDate: { $gte: new Date() },
$or: [{ 'departure.city': cityRegex }, { 'destination.city': cityRegex }, { 'waypoints.location.city': cityRegex }],
};
@@ -531,7 +530,7 @@ RideSchema.statics.findPassingThrough = function (idapp, cityName, options = {})
return this.find(query)
.populate('userId', 'username name surname profile.driverProfile.averageRating')
.sort({ dateTime: 1 });
.sort({ departureDate: 1 });
};
const Ride = mongoose.model('Ride', RideSchema);

View File

@@ -250,7 +250,7 @@ RideRequestSchema.statics.getPendingForDriver = function(idapp, driverId) {
status: 'pending'
})
.populate('passengerId', 'username name surname email')
.populate('rideId', 'departure destination dateTime')
.populate('rideId', 'departure destination departureDate')
.sort({ createdAt: -1 });
};

View File

@@ -0,0 +1,310 @@
// ============================================================
// 🔧 USER SETTINGS MODEL - Trasporti Solidali
// ============================================================
// File: server/models/viaggi/UserSettings.js
const mongoose = require('mongoose');
const userSettingsSchema = new mongoose.Schema({
// ID App e Utente
idapp: {
type: String,
required: true,
index: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User',
index: true
},
// ============================================================
// 🔔 NOTIFICHE
// ============================================================
notifications: {
// Notifiche Email
email: {
newMessage: { type: Boolean, default: true },
rideRequest: { type: Boolean, default: true },
rideConfirmation: { type: Boolean, default: true },
rideCancellation: { type: Boolean, default: true },
rideReminder: { type: Boolean, default: true },
feedbackReceived: { type: Boolean, default: true },
newsletter: { type: Boolean, default: false }
},
// Notifiche Push
push: {
newMessage: { type: Boolean, default: true },
rideRequest: { type: Boolean, default: true },
rideConfirmation: { type: Boolean, default: true },
rideCancellation: { type: Boolean, default: true },
rideReminder: { type: Boolean, default: true },
feedbackReceived: { type: Boolean, default: true }
},
// Notifiche In-App
inApp: {
newMessage: { type: Boolean, default: true },
rideRequest: { type: Boolean, default: true },
rideConfirmation: { type: Boolean, default: true },
rideCancellation: { type: Boolean, default: true }
}
},
// ============================================================
// 🔒 PRIVACY
// ============================================================
privacy: {
// Visibilità profilo
profileVisibility: {
type: String,
enum: ['public', 'members', 'private'],
default: 'members'
},
// Mostra informazioni di contatto
showPhone: { type: Boolean, default: false },
showEmail: { type: Boolean, default: false },
// Mostra statistiche profilo
showStats: { type: Boolean, default: true },
// Mostra feedback ricevuti
showFeedbacks: { type: Boolean, default: true },
// Condividi posizione durante viaggio
shareLocation: { type: Boolean, default: true },
// Chi può contattarmi
whoCanContact: {
type: String,
enum: ['everyone', 'verified', 'afterBooking'],
default: 'verified'
}
},
// ============================================================
// 🚗 PREFERENZE VIAGGI
// ============================================================
ridePreferences: {
// Preferenze come conducente
driver: {
// Accetta prenotazioni istantanee
instantBooking: { type: Boolean, default: false },
// Richiede verifica documento passeggeri
requireVerification: { type: Boolean, default: false },
// Conversazione durante il viaggio
chattiness: {
type: String,
enum: ['silent', 'moderate', 'chatty', 'any'],
default: 'any'
},
// Musica
music: {
type: String,
enum: ['no', 'soft', 'any'],
default: 'any'
},
// Fumatori
smoking: {
type: String,
enum: ['no', 'outside', 'yes'],
default: 'no'
},
// Animali
pets: {
type: String,
enum: ['no', 'small', 'yes'],
default: 'no'
},
// Bagagli extra
luggage: {
type: String,
enum: ['small', 'medium', 'large'],
default: 'medium'
}
},
// Preferenze come passeggero
passenger: {
// Conversazione
chattiness: {
type: String,
enum: ['silent', 'moderate', 'chatty', 'any'],
default: 'any'
},
// Musica
music: {
type: String,
enum: ['no', 'soft', 'any'],
default: 'any'
},
// Fumatori
smokingTolerance: {
type: String,
enum: ['no', 'outside', 'yes'],
default: 'no'
},
// Viaggio con animali
comfortableWithPets: { type: Boolean, default: true }
}
},
// ============================================================
// 🔍 RICERCA & FILTRI PREDEFINITI
// ============================================================
searchPreferences: {
// Raggio di ricerca predefinito (km)
defaultRadius: { type: Number, default: 50 },
// Ordine risultati
defaultSortBy: {
type: String,
enum: ['date', 'price', 'distance', 'rating'],
default: 'date'
},
// Solo viaggi verificati
verifiedOnly: { type: Boolean, default: false },
// Solo con recensioni positive
minRating: { type: Number, min: 0, max: 5, default: 0 }
},
// ============================================================
// 💳 PAGAMENTI & DONAZIONI
// ============================================================
payment: {
// Metodo di pagamento predefinito
defaultMethod: {
type: String,
enum: ['cash', 'card', 'app', 'none'],
default: 'cash'
},
// Contributo suggerito automatico
autoSuggestContribution: { type: Boolean, default: true },
// Accetta pagamenti anticipati
acceptAdvancePayment: { type: Boolean, default: false }
},
// ============================================================
// 📱 INTERFACCIA
// ============================================================
interface: {
// Tema
theme: {
type: String,
enum: ['light', 'dark', 'auto'],
default: 'auto'
},
// Lingua
language: {
type: String,
enum: ['it', 'en', 'de', 'fr', 'es'],
default: 'it'
},
// Mostra tutorial
showTutorials: { type: Boolean, default: true },
// Vista mappa predefinita
defaultMapView: { type: Boolean, default: false }
},
// ============================================================
// 🔐 SICUREZZA
// ============================================================
security: {
// Richiedi verifica telefono per prenotazioni
requirePhoneVerification: { type: Boolean, default: true },
// Autenticazione a due fattori
twoFactorAuth: { type: Boolean, default: false },
// Logout automatico dopo inattività (minuti)
autoLogout: { type: Number, default: 30 },
// Richiedi conferma prima di cancellare viaggio
confirmBeforeCancel: { type: Boolean, default: true }
}
}, {
timestamps: true
});
// ============================================================
// 📊 INDICI
// ============================================================
userSettingsSchema.index({ idapp: 1, userId: 1 }, { unique: true });
// ============================================================
// 🎯 METODI STATICI
// ============================================================
/**
* Ottieni o crea impostazioni utente con valori predefiniti
*/
userSettingsSchema.statics.getOrCreateSettings = async function(idapp, userId) {
let settings = await this.findOne({ idapp, userId });
if (!settings) {
settings = await this.create({
idapp,
userId,
// I valori predefiniti sono già definiti nello schema
});
}
return settings;
};
/**
* Aggiorna impostazioni parziali
*/
userSettingsSchema.statics.updateSettings = async function(idapp, userId, updates) {
const settings = await this.getOrCreateSettings(idapp, userId);
// Merge delle impostazioni
Object.keys(updates).forEach(section => {
if (settings[section] && typeof updates[section] === 'object') {
settings[section] = {
...settings[section],
...updates[section]
};
} else {
settings[section] = updates[section];
}
});
await settings.save();
return settings;
};
// ============================================================
// 🎯 METODI ISTANZA
// ============================================================
/**
* Verifica se una notifica è abilitata
*/
userSettingsSchema.methods.isNotificationEnabled = function(type, channel) {
if (!this.notifications[channel]) return false;
return this.notifications[channel][type] !== false;
};
/**
* Ottieni preferenze compatibilità viaggio
*/
userSettingsSchema.methods.getCompatibilityPreferences = function(asRole = 'passenger') {
if (asRole === 'driver') {
return this.ridePreferences.driver;
}
return this.ridePreferences.passenger;
};
/**
* Esporta impostazioni per frontend
*/
userSettingsSchema.methods.toClientJSON = function() {
return {
notifications: this.notifications,
privacy: this.privacy,
ridePreferences: this.ridePreferences,
searchPreferences: this.searchPreferences,
payment: this.payment,
interface: this.interface,
security: {
requirePhoneVerification: this.security.requirePhoneVerification,
twoFactorAuth: this.security.twoFactorAuth,
confirmBeforeCancel: this.security.confirmBeforeCancel
}
};
};
module.exports = mongoose.model('TrasportiUserSettings', userSettingsSchema);

View File

@@ -463,6 +463,8 @@ async function aggiornaCategorieESottoCategorie() {
async function runMigration() {
try {
const { User } = require('../models/user');
const idapp = 0; // TUTTI
console.log('🚀 Controllo Versioni Tabelle (runMigration)');
@@ -471,6 +473,10 @@ async function runMigration() {
idapp,
shared_consts.JOB_TO_EXECUTE.MIGRATION_SECTORS_DIC25
);
const isMigratione30Dic2025Telegram = await Version.isJobExecuted(
idapp,
shared_consts.JOB_TO_EXECUTE.MIGRATION_TELEGRAM_30DIC25
);
const vers_server_str = await tools.getVersServer();
@@ -522,6 +528,18 @@ async function runMigration() {
console.log('\n✅ Migrazione DIC 2025 completata con successo!');
}
if (isMigratione30Dic2025Telegram) {
await User.updateMany({ 'profile.teleg_id': { $exists: true, $ne: 0 } }, [
{
$set: {
'notificationPreferences.telegram.enabled': true,
'notificationPreferences.telegram.chatId': '$profile.teleg_id',
},
},
]);
await Version.setJobExecuted(idapp, shared_consts.JOB_TO_EXECUTE.MIGRATION_TELEGRAM_30DIC25);
}
await Version.setLastVersionRun(idapp, version_server);
} catch (error) {
console.error('❌ Errore durante la migrazione:', error);
@@ -535,5 +553,5 @@ module.exports = {
subSkillMapping,
sectorGoodMapping,
sectorBachecaMapping,
aggiornaCategorieESottoCategorie,
aggiornaCategorieESottoCategorie,
};

View File

@@ -32,9 +32,16 @@ const { MyElem } = require('../models/myelem');
const axios = require('axios');
const settingsRoutes = require('../routes/viaggi/settingsRoutes');
router.use('/viaggi/settings', settingsRoutes);
const widgetRoutes = require('../routes/viaggi/widgetRoutes');
router.use('/viaggi/widget', widgetRoutes);
const viaggiRoutes = require('../routes/viaggiRoutes');
router.use('/viaggi', viaggiRoutes);
// Importa le routes video
const videoRoutes = require('../routes/videoRoutes');

View File

@@ -0,0 +1,100 @@
// ============================================================
// 🔧 SETTINGS ROUTES - Trasporti Solidali
// ============================================================
// File: server/routes/viaggi/settingsRoutes.js
const express = require('express');
const router = express.Router();
const settingsController = require('../../controllers/viaggi/settingsController');
const { authenticate } = require('../../middleware/authenticate');
// ============================================================
// 🔐 TUTTE LE ROUTES RICHIEDONO AUTENTICAZIONE
// ============================================================
router.use(authenticate);
// ============================================================
// 📄 IMPOSTAZIONI GENERALI
// ============================================================
/**
* GET /api/viaggi/settings
* Ottieni tutte le impostazioni dell'utente
*/
router.get('/', settingsController.getSettings);
/**
* PUT /api/viaggi/settings
* Aggiorna le impostazioni (completo)
*/
router.put('/', settingsController.updateSettings);
/**
* POST /api/viaggi/settings/reset
* Reset impostazioni ai valori predefiniti
*/
router.post('/reset', settingsController.resetSettings);
// ============================================================
// 📝 AGGIORNAMENTI PARZIALI (per sezione)
// ============================================================
const notifController = require('../../controllers/viaggi/trasportiNotificationsController');
// Preferenze
router.get('/notifications/preferences', authenticate, notifController.getNotificationPreferences);
router.put('/notifications/preferences', authenticate, notifController.updateNotificationPreferences);
// Telegram
router.post('/notifications/telegram/code', authenticate, notifController.generateTelegramCode);
router.post('/notifications/telegram/connect', authenticate, notifController.connectTelegram);
router.delete('/notifications/telegram/disconnect', authenticate, notifController.disconnectTelegram);
// Push
router.post('/notifications/push/subscribe', authenticate, notifController.subscribePushNotifications);
router.delete('/notifications/push/unsubscribe', authenticate, notifController.unsubscribePushNotifications);
// Test
router.post('/notifications/test', authenticate, notifController.sendTestNotification);
/**
* PATCH /api/viaggi/settings/notifications
* Aggiorna solo le notifiche
*/
router.patch('/notifications', settingsController.updateNotifications);
/**
* PATCH /api/viaggi/settings/privacy
* Aggiorna solo la privacy
*/
router.patch('/privacy', settingsController.updatePrivacy);
/**
* PATCH /api/viaggi/settings/ride-preferences
* Aggiorna solo le preferenze viaggi
*/
router.patch('/ride-preferences', settingsController.updateRidePreferences);
/**
* PATCH /api/viaggi/settings/interface
* Aggiorna solo l'interfaccia
*/
router.patch('/interface', settingsController.updateInterface);
// ============================================================
// 📊 EXPORT / IMPORT
// ============================================================
/**
* GET /api/viaggi/settings/export
* Esporta tutte le impostazioni
*/
router.get('/export', settingsController.exportSettings);
/**
* POST /api/viaggi/settings/import
* Importa impostazioni da backup
*/
router.post('/import', settingsController.importSettings);
module.exports = router;

View File

@@ -0,0 +1,32 @@
// ============================================================
// 📊 WIDGET & STATS ROUTES - Trasporti Solidali
// ============================================================
// File: server/routes/viaggi/widgetRoutes.js
const express = require('express');
const router = express.Router();
const widgetController = require('../../controllers/viaggi/widgetController');
const { authenticate } = require('../../middleware/authenticate');
// ============================================================
// 🔐 TUTTE LE ROUTES RICHIEDONO AUTENTICAZIONE
// ============================================================
router.use(authenticate);
// ============================================================
// 📊 WIDGET DATA
// ============================================================
/**
* GET /api/viaggi/widget/data
* Ottieni tutti i dati per il widget dashboard
*/
router.get('/data', widgetController.getWidgetData);
/**
* GET /api/viaggi/widget/stats
* Statistiche rapide per badge/notifiche
*/
router.get('/stats', widgetController.getQuickStats);
module.exports = router;

View File

@@ -20,7 +20,6 @@ const geoRoutes = require('./geoRoutes'); // 👈 Importa geoRoutes
router.use('/geo', geoRoutes); // 👈 Monta come sub-router
// Middleware di autenticazione (usa il tuo esistente)
const { authenticate } = require('../middleware/authenticate');
@@ -42,6 +41,7 @@ router.post('/rides', authenticate, rideController.createRide);
*/
router.get('/rides', rideController.getRides);
/**
* @route GET /api/viaggi/rides/search
* @desc Ricerca viaggi avanzata
@@ -71,6 +71,18 @@ router.get('/rides/my', authenticate, rideController.getMyRides);
*/
//router.get('/rides/match', authenticate, rideController.findMatches);
router.get('/rides/cancelled', authenticate, rideController.getCancelledRides);
router.get('/rides/statscomm', authenticate, rideController.getCommunityStatsComm);
router.get('/rides/community', authenticate, rideController.getCommunityRides);
router.get('/rides/calendar', authenticate, rideController.getCalendarRides);
/**
* @route POST /api/viaggi/rides/:rideId/request
* @desc Richiedi posto su un viaggio
* @access Private
*/
router.post('/rides/:rideId/request', authenticate, rideRequestController.createRequestFromRide);
/**
* @route GET /api/viaggi/rides/:id
* @desc Dettaglio singolo viaggio
@@ -99,6 +111,8 @@ router.delete('/rides/:id', authenticate, rideController.deleteRide);
*/
router.post('/rides/:id/complete', authenticate, rideController.completeRide);
router.post('/rides/:rideId/favorite', authenticate, rideController.toggleFavoriteRide);
// ============================================================
// 📊 WIDGET & STATS
// ============================================================
@@ -110,13 +124,6 @@ router.post('/rides/:id/complete', authenticate, rideController.completeRide);
*/
router.get('/widget/data', authenticate, rideController.getWidgetData);
/**
* @route GET /api/viaggi/stats/summary
* @desc Stats rapide per header widget (offerte, richieste, match)
* @access Public
*/
router.get('/stats/summary', authenticate, rideController.getStatsSummary);
/**
* @route GET /api/viaggi/cities/suggestions
* @desc Suggerimenti città per autocomplete (basato su viaggi esistenti)
@@ -434,8 +441,8 @@ router.get('/driver/user/:userId', async (req, res) => {
const idapp = req.query.idapp;
const { User } = require('../models/user');
const Ride = require('../models/Ride');
const Feedback = require('../models/Feedback');
const Ride = require('../models/viaggi/Ride');
const Feedback = require('../models/viaggi/Feedback');
// Dati utente
const user = await User.findById(userId).select(
@@ -463,8 +470,8 @@ router.get('/driver/user/:userId', async (req, res) => {
type: 'offer',
status: { $in: ['active', 'completed'] },
})
.select('departure destination dateTime status')
.sort({ dateTime: -1 })
.select('departure destination departureDate status')
.sort({ departureDate: -1 })
.limit(5);
// Statistiche feedback
@@ -1037,4 +1044,9 @@ router.delete('/upload/vehicle-photo', authenticate, async (req, res) => {
}
});
// ============================================
// EXPORT ROUTES
// ============================================
module.exports = router;

View File

@@ -1,57 +0,0 @@
const fs = require('fs');
const path = require('path');
const pty = require('node-pty');
const { User } = require('../models/user');
const { sendMessage } = require('../telegram/api');
function setupShellWebSocket(ws) {
console.log('🔌 Client WebSocket Shell connesso');
let scriptProcess = null;
let buffer = '';
ws.on('message', async (message) => {
try {
const parsed = JSON.parse(message);
const { type, user_id, scriptName, data } = parsed;
if (type === 'start_script' && (await User.isAdminById(user_id))) {
if (scriptProcess) scriptProcess.kill();
const scriptPath = path.join(__dirname, '..', '..', scriptName);
if (!fs.existsSync(scriptPath)) {
return ws.send(JSON.stringify({ type: 'error', data: 'Script non trovato' }));
}
scriptProcess = pty.spawn('bash', [scriptPath], {
name: 'xterm-color',
cols: 80,
rows: 40,
cwd: process.cwd(),
env: process.env,
});
scriptProcess.on('data', (chunk) => {
buffer += chunk;
ws.send(JSON.stringify({ type: 'output', data: chunk }));
if (buffer.length > 4096) buffer = buffer.slice(-2048);
});
scriptProcess.on('exit', (code) => {
const msg = code === 0 ? '✅ Script completato' : `❌ Uscito con codice ${code}`;
ws.send(JSON.stringify({ type: 'close', data: msg }));
});
} else if (type === 'input' && scriptProcess) {
scriptProcess.write(data + '\n');
}
} catch (err) {
console.error('❌ Errore WS Shell:', err.message);
}
});
ws.on('close', () => {
if (scriptProcess) scriptProcess.kill();
console.log('🔌 WS Shell chiuso');
});
}
module.exports = { setupShellWebSocket };

View File

@@ -1,33 +0,0 @@
const axios = require('axios');
const { API_URL, TIMEOUT } = require('./config');
async function callTelegram(method, params) {
try {
const { data } = await axios.post(`${API_URL}/${method}`, params, { timeout: TIMEOUT });
if (!data.ok) throw new Error(`Telegram error: ${data.description}`);
return data.result;
} catch (err) {
console.error('❌ Telegram API error:', err.message);
return null;
}
}
async function sendMessage(chatId, text, options = {}) {
return callTelegram('sendMessage', {
chat_id: chatId,
text,
parse_mode: options.parse_mode || 'HTML',
disable_web_page_preview: true,
});
}
async function sendPhoto(chatId, photo, caption = '', options = {}) {
return callTelegram('sendPhoto', {
chat_id: chatId,
photo,
caption,
parse_mode: 'HTML',
});
}
module.exports = { sendMessage, sendPhoto };

View File

@@ -1,8 +0,0 @@
module.exports = {
TOKEN: process.env.TELEGRAM_BOT_TOKEN,
API_URL: `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}`,
ADMIN_GROUP_IDS: process.env.TELEGRAM_ADMIN_GROUPS
? process.env.TELEGRAM_ADMIN_GROUPS.split(',')
: [],
TIMEOUT: 5000,
};

View File

@@ -1,18 +0,0 @@
// Ruoli, fasi logiche e costanti admin (adatta gli ID ai tuoi reali)
module.exports = {
ADMIN_USER_SERVER: process.env.ADMIN_USER_SERVER || 'server_admin',
ADMIN_IDTELEGRAM_SERVER: process.env.ADMIN_IDTELEGRAM_SERVER || '',
phase: {
REGISTRATION: 'REGISTRATION',
REGISTRATION_CONFIRMED: 'REGISTRATION_CONFIRMED',
RESET_PWD: 'RESET_PWD',
NOTIFICATION: 'NOTIFICATION',
GENERIC: 'GENERIC',
},
roles: {
ADMIN: 'ADMIN',
MANAGER: 'MANAGER',
FACILITATORE: 'FACILITATORE',
EDITOR: 'EDITOR',
},
};

View File

@@ -1,11 +0,0 @@
const { sendMessage } = require('../api');
const { ADMIN_GROUP_IDS } = require('../config');
const { safeExec } = require('../helpers');
const sendToAdmins = safeExec(async (message) => {
for (const id of ADMIN_GROUP_IDS) {
await sendMessage(id, message);
}
});
module.exports = { sendToAdmins };

View File

@@ -1,101 +0,0 @@
// telegram/handlers/callbackHandler.js
const tools = require('../../tools/general');
const shared_consts = require('../../tools/shared_nodejs');
const { User } = require('../../models/user');
const { Circuit } = require('../../models/circuit');
const { handleRegistration } = require('./registrationHandler');
const { handleFriends } = require('./friendsHandler');
const { handleCircuit } = require('./circuitHandler');
const { handleZoom } = require('./zoomHandler');
const { handlePassword } = require('./passwordHandler');
async function handleCallback(bot, cl, callbackQuery) {
const idapp = cl.idapp;
let notifyText = ''; // testo di notifica Telegram (answerCallbackQuery)
try {
// parsing payload dal tuo formato originale (action|username|userDest|groupId|circuitId|groupname)
let data = {
action: '',
username: '',
userDest: '',
groupId: '',
circuitId: '',
groupname: '',
};
const raw = callbackQuery?.data || '';
if (raw) {
const arr = raw.split(tools.SEP);
data = {
action: arr[0] || '',
username: arr[1] || '',
userDest: arr[2] || '',
groupId: arr[3] || '',
circuitId: arr[4] || '',
groupname: arr[5] || '',
};
}
// normalizza username reali (come nel sorgente)
data.username = await User.getRealUsernameByUsername(idapp, data.username);
data.userDest = data.userDest ? await User.getRealUsernameByUsername(idapp, data.userDest) : '';
const msg = callbackQuery.message;
const opts = { chat_id: msg.chat.id, message_id: msg.message_id };
// contest utente corrente
await cl.setInit?.(msg); // se presente nel tuo codice
const rec = cl.getRecInMem?.(msg);
const username_action = rec?.user ? rec.user.username : '';
// carica user e userDest compatti (come nel tuo codice)
const user = data.username ? await User.getUserShortDataByUsername(idapp, data.username) : null;
const userDest = data.userDest ? await User.getUserShortDataByUsername(idapp, data.userDest) : null;
// routing per ambito
const act = data.action || '';
// 1) REGISTRAZIONE e varianti
if (act.includes(shared_consts.CallFunz.REGISTRATION)) {
notifyText = await handleRegistration({ bot, cl, idapp, msg, data, user, userDest, username_action });
}
// 2) AMICIZIA / HANDSHAKE
else if (
act.includes(shared_consts.CallFunz.RICHIESTA_AMICIZIA) ||
act.includes(shared_consts.CallFunz.RICHIESTA_HANDSHAKE)
) {
notifyText = await handleFriends({ bot, cl, idapp, msg, data, user, userDest, username_action });
}
// 3) CIRCUITI (aggiunta/rimozione)
else if (
act.includes(shared_consts.CallFunz.ADDUSERTOCIRCUIT) ||
act.includes(shared_consts.CallFunz.REMUSERFROMCIRCUIT)
) {
notifyText = await handleCircuit({ bot, cl, idapp, msg, data, user, userDest, username_action });
}
// 4) ZOOM (registrazione/presenze)
else if (act.includes(shared_consts.CallFunz.REGISTRATION_TOZOOM) || act.includes('ZOOM')) {
notifyText = await handleZoom({ bot, cl, idapp, msg, data, user, userDest, username_action });
}
// 5) RESET PASSWORD
else if (act.includes(shared_consts.CallFunz.RESET_PWD)) {
notifyText = await handlePassword({ bot, cl, idapp, msg, data, user, userDest, username_action });
} else if (act.includes(shared_consts.CallFunz.RICHIESTA_GRUPPO)) {
notifyText = await handleGroup({ bot, cl, idapp, msg, data, user, userDest, username_action });
}
// default
else {
notifyText = 'Operazione completata';
await cl.sendMsg(msg.chat.id, `⚙️ Azione non riconosciuta: ${act}`);
}
await bot.answerCallbackQuery(callbackQuery.id, { text: notifyText || 'OK' });
} catch (err) {
console.error('❌ callbackHandler error:', err.message);
try {
await bot.answerCallbackQuery(callbackQuery.id, { text: 'Errore', show_alert: true });
} catch (_) {}
}
}
module.exports = { handleCallback };

View File

@@ -1,54 +0,0 @@
// telegram/handlers/circuitHandler.js
const shared_consts = require('../../tools/shared_nodejs');
const tools = require('../../tools/general');
const { User } = require('../../models/user');
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
async function handleCircuit({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
let notifyText = '';
// Aggiunta al circuito
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.ADDUSERTOCIRCUIT) {
const cmd = shared_consts.CIRCUITCMD.ADDUSERTOCIRCUIT;
const req = tools.getReqByPar(idapp, username_action);
// se viene da gruppo usa ifCircuitAlreadyInGroup, altrimenti ifAlreadyInCircuit (come nel tuo codice)
const already = data.groupname
? await User.ifCircuitAlreadyInGroup(idapp, data.groupname, data.circuitId)
: await User.ifAlreadyInCircuit(idapp, data.username, data.circuitId);
if (!already) {
await User.setCircuitCmd(idapp, data.username, data.circuitId, cmd, 1, username_action, { groupname: data.groupname });
await cl.sendMsg(msg.chat.id, `${data.username} aggiunto al circuito ${data.circuitId}`);
notifyText = 'Circuito OK';
} else {
await cl.sendMsg(msg.chat.id, ` ${data.username} è già nel circuito ${data.circuitId}`);
notifyText = 'Già presente';
}
return notifyText;
}
// Rimozione dal circuito
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REMUSERFROMCIRCUIT) {
const cmd = shared_consts.CIRCUITCMD.REMOVEUSERFROMCIRCUIT;
await User.setCircuitCmd(idapp, data.username, data.circuitId, cmd, 0, username_action, { groupname: data.groupname });
await cl.sendMsg(msg.chat.id, `🗑️ ${data.username} rimosso dal circuito ${data.circuitId}`);
notifyText = 'Rimosso';
return notifyText;
}
// NO / annulla
if (
data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.ADDUSERTOCIRCUIT ||
data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REMUSERFROMCIRCUIT
) {
await cl.sendMsg(msg.chat.id, '❌ Operazione circuito annullata.');
notifyText = 'Annullata';
return notifyText;
}
return 'OK';
}
module.exports = { handleCircuit };

View File

@@ -1,24 +0,0 @@
const { sendMessage, sendPhoto } = require('../api');
const { safeExec } = require('../helpers');
const sendMsgTelegram = safeExec(async (user, text) => {
if (!user || !user.telegram_id) return null;
return sendMessage(user.telegram_id, text);
});
const sendMsgTelegramByIdTelegram = safeExec(async (telegramId, text) => {
if (!telegramId) return null;
return sendMessage(telegramId, text);
});
const sendPhotoTelegram = safeExec(async (chatIdOrUser, photoUrl, caption = '') => {
const chatId = typeof chatIdOrUser === 'object' ? chatIdOrUser?.telegram_id : chatIdOrUser;
if (!chatId || !photoUrl) return null;
return sendPhoto(chatId, photoUrl, caption);
});
module.exports = {
sendMsgTelegram,
sendMsgTelegramByIdTelegram,
sendPhotoTelegram,
};

View File

@@ -1,12 +0,0 @@
const { sendMessage } = require('../api');
const { ADMIN_GROUP_IDS } = require('../config');
const { safeExec } = require('../helpers');
const reportError = safeExec(async (context, err) => {
const msg = `🚨 Errore in <b>${context}</b>\n<pre>${err.stack || err.message}</pre>`;
for (const id of ADMIN_GROUP_IDS) {
await sendMessage(id, msg);
}
});
module.exports = { reportError };

View File

@@ -1,61 +0,0 @@
// telegram/handlers/friendsHandler.js
const shared_consts = require('../../tools/shared_nodejs');
const tools = require('../../tools/general');
const { User } = require('../../models/user');
const printf = require('util').format;
const { handleRegistration, InlineConferma } = require('./registrationHandler');
async function handleFriends({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
let notifyText = '';
// SI -> amicizia
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_AMICIZIA) {
if (userDest) {
const req = tools.getReqByPar(idapp, username_action);
const already = await User.isMyFriend(idapp, data.username, data.userDest);
if (!already) await User.setFriendsCmd(req, idapp, data.username, data.userDest, shared_consts.FRIENDSCMD.SETFRIEND);
await cl.sendMsg(msg.chat.id, '🤝 Amicizia confermata!');
notifyText = 'Amicizia OK';
}
return notifyText;
}
// NO -> amicizia (rimuovi/nega)
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RICHIESTA_AMICIZIA) {
if (userDest) {
const req = tools.getReqByPar(idapp, username_action);
const ris = await User.setFriendsCmd(req, idapp, data.username, data.userDest, shared_consts.FRIENDSCMD.REMOVE_FROM_MYFRIENDS);
if (ris) {
const msgDest = printf(tools.gettranslate('MSG_FRIENDS_NOT_ACCEPTED_CONFIRMED', user.lang), data.username);
await localSendMsgByUsername(idapp, data.userDest, msgDest);
}
await cl.sendMsg(msg.chat.id, '🚫 Amicizia rifiutata.');
notifyText = 'Rifiutata';
}
return notifyText;
}
// SI -> handshake
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_HANDSHAKE) {
if (userDest) {
const req = tools.getReqByPar(idapp, username_action);
const already = await User.isMyHandShake(idapp, data.userDest, data.username);
if (!already) await User.setFriendsCmd(req, idapp, data.userDest, data.username, shared_consts.FRIENDSCMD.SETHANDSHAKE);
await cl.sendMsg(msg.chat.id, '🤝 Handshake confermato!');
notifyText = 'Handshake OK';
}
return notifyText;
}
return 'OK';
}
// helper locale (equivalente del tuo local_sendMsgTelegram)
async function localSendMsgByUsername(idapp, username, text) {
const teleg_id = await User.TelegIdByUsername(idapp, username);
const cl = require('../telegram.bot.init').getclTelegByidapp(idapp);
if (cl && teleg_id) return await cl.sendMsg(teleg_id, text);
return null;
}
module.exports = { handleFriends };

View File

@@ -1,70 +0,0 @@
// telegram/handlers/groupHandler.js
const shared_consts = require('../../tools/shared_nodejs');
const tools = require('../../tools/general');
const { User } = require('../../models/user');
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
/**
* Gestisce conferma/rifiuto a richieste di GRUPPO
* Payload data:
* - action
* - username (mittente originale)
* - userDest (destinatario/utente da aggiungere)
* - groupId (id o path del gruppo)
* - groupname (nome del gruppo)
*/
async function handleGroup({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
let notifyText = '';
// SI → accetta richiesta d'ingresso nel gruppo
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_GRUPPO) {
// Se lapp ha funzioni di persistenza specifiche, usale se esistono
// (non assumo nomi rigidi per non rompere il deploy)
if (typeof User.setGroupCmd === 'function') {
try {
await User.setGroupCmd(idapp, data.userDest || data.username, data.groupId, shared_consts.GROUPCMD.ADDUSERTOGROUP, 1, username_action, { groupname: data.groupname });
} catch (e) {
console.warn('handleGroup:setGroupCmd non eseguito:', e.message);
}
}
// Messaggi di conferma
await cl.sendMsg(msg.chat.id, `${data.userDest || data.username} è stato aggiunto al gruppo ${data.groupname || data.groupId}.`);
// Notifica anche lutente interessato
const targetUsername = data.userDest || data.username;
const teleg_id = await User.TelegIdByUsername(idapp, targetUsername);
if (teleg_id) {
await cl.sendMsg(teleg_id, `👥 Sei stato aggiunto al gruppo: ${data.groupname || data.groupId}`);
}
notifyText = 'Gruppo: aggiunta OK';
return notifyText;
}
// NO → rifiuta/annulla richiesta
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RICHIESTA_GRUPPO) {
if (typeof User.setGroupCmd === 'function') {
try {
await User.setGroupCmd(idapp, data.userDest || data.username, data.groupId, shared_consts.GROUPCMD.REMOVEUSERFROMGROUP, 0, username_action, { groupname: data.groupname });
} catch (e) {
console.warn('handleGroup:setGroupCmd non eseguito:', e.message);
}
}
await cl.sendMsg(msg.chat.id, '🚫 Richiesta gruppo rifiutata.');
// Avvisa il richiedente
const targetUsername = data.userDest || data.username;
const teleg_id = await User.TelegIdByUsername(idapp, targetUsername);
if (teleg_id) {
await cl.sendMsg(teleg_id, `❌ La tua richiesta per il gruppo ${data.groupname || data.groupId} è stata rifiutata.`);
}
notifyText = 'Gruppo: rifiutata';
return notifyText;
}
return 'OK';
}
module.exports = { handleGroup };

View File

@@ -1,47 +0,0 @@
const { sendMessage } = require('../api');
const { safeExec, eachSeries } = require('../helpers');
const tools = require('../../tools/general');
const {
getAdminTelegramUsers,
getManagersTelegramUsers,
getAllTelegramUsersByApp,
} = require('./userQuery');
const sendMsgTelegramToTheAdminAllSites = safeExec(async (text, alsoGroups = false) => {
const apps = await tools.getApps(); // deve restituire {idapp,...}
await eachSeries(apps, async (app) => {
const admins = await getAdminTelegramUsers(app.idapp);
await eachSeries(admins, async (u) => sendMessage(u.telegram_id, text));
if (alsoGroups && app?.telegram_admin_group_id) {
await sendMessage(app.telegram_admin_group_id, text);
}
});
});
const sendMsgTelegramByIdApp = safeExec(async (idapp, text) => {
const users = await getAllTelegramUsersByApp(idapp);
await eachSeries(users, async (u) => sendMessage(u.telegram_id, text));
});
const sendMsgTelegramToTheManagers = safeExec(async (idapp, text) => {
const managers = await getManagersTelegramUsers(idapp);
await eachSeries(managers, async (u) => sendMessage(u.telegram_id, text));
});
const sendMsgTelegramToTheAdmin = safeExec(async (idapp, text) => {
const admins = await getAdminTelegramUsers(idapp);
await eachSeries(admins, async (u) => sendMessage(u.telegram_id, text));
});
const sendMsgTelegramToTheGroup = safeExec(async (chatId, text) => {
if (!chatId) return null;
return sendMessage(chatId, text);
});
module.exports = {
sendMsgTelegramToTheAdminAllSites,
sendMsgTelegramByIdApp,
sendMsgTelegramToTheManagers,
sendMsgTelegramToTheAdmin,
sendMsgTelegramToTheGroup,
};

View File

@@ -1,9 +0,0 @@
const { sendMessage } = require('../api');
const { safeExec } = require('../helpers');
const sendNotification = safeExec(async (chatId, title, body) => {
const msg = `🔔 <b>${title}</b>\n${body}`;
await sendMessage(chatId, msg);
});
module.exports = { sendNotification };

View File

@@ -1,35 +0,0 @@
// telegram/handlers/passwordHandler.js
const shared_consts = require('../../tools/shared_nodejs');
const tools = require('../../tools/general');
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
async function handlePassword({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
let notifyText = '';
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RESET_PWD) {
// Nel tuo codice usavi anche tools.sendNotificationToUser ecc.
await tools.sendNotificationToUser(
user?._id || msg.chat.id,
'🔑 Reset Password',
`La password di ${data.username} è stata resettata.`,
'/',
'',
'server',
[]
);
await cl.sendMsg(msg.chat.id, '✅ Password resettata.');
notifyText = 'Reset OK';
return notifyText;
}
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RESET_PWD) {
await cl.sendMsg(msg.chat.id, '❌ Reset password annullato.');
notifyText = 'Annullato';
return notifyText;
}
return 'OK';
}
module.exports = { handlePassword };

View File

@@ -1,65 +0,0 @@
const messages = require('../messages');
const { phase } = require('../constants');
const { safeExec } = require('../helpers');
const { sendMessage } = require('../api');
const {
getAdminTelegramUsers,
getManagersTelegramUsers,
} = require('./userQuery');
// locals: { idapp, username, nomeapp, text, ... }
const notifyToTelegram = safeExec(async (ph, locals = {}) => {
const idapp = String(locals.idapp || '');
let text = '';
const templ = messages.byPhase[ph] || messages.byPhase.GENERIC;
text = templ(locals);
// router di default: manda agli admin dell'app
const admins = await getAdminTelegramUsers(idapp);
for (const a of admins) {
if (a.telegram_id) await sendMessage(a.telegram_id, text);
}
});
const askConfirmationUser = safeExec(async (idapp, phaseCode, user) => {
const txt = messages.askConfirmationUser({
idapp,
username: user?.username,
nomeapp: user?.nomeapp,
});
if (user?.telegram_id) await sendMessage(user.telegram_id, txt);
});
// helper semplici
const sendNotifToAdmin = safeExec(async (idapp, title, body = '') => {
const admins = await getAdminTelegramUsers(String(idapp));
const txt = `📣 <b>${title}</b>\n${body}`;
for (const a of admins) {
if (a.telegram_id) await sendMessage(a.telegram_id, txt);
}
});
const sendNotifToManager = safeExec(async (idapp, title, body = '') => {
const managers = await getManagersTelegramUsers(String(idapp));
const txt = `📣 <b>${title}</b>\n${body}`;
for (const m of managers) {
if (m.telegram_id) await sendMessage(m.telegram_id, txt);
}
});
const sendNotifToAdminOrManager = safeExec(async (idapp, title, body = '', preferManagers = false) => {
if (preferManagers) {
return sendNotifToManager(idapp, title, body);
}
return sendNotifToAdmin(idapp, title, body);
});
module.exports = {
notifyToTelegram,
askConfirmationUser,
sendNotifToAdmin,
sendNotifToManager,
sendNotifToAdminOrManager,
phase, // re-export utile
};

View File

@@ -1,50 +0,0 @@
// telegram/handlers/registrationHandler.js
const shared_consts = require('../../tools/shared_nodejs');
const { User } = require('../../models/user');
const telegrambot = require('../telegram.bot.init'); // per sendMsgTelegramToTheAdminAllSites
const printf = require('util').format;
const InlineConferma = {
RISPOSTA_SI: 'SI_',
RISPOSTA_NO: 'NO_',
};
async function handleRegistration({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
let notifyText = '';
// NO alla registrazione
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION) {
await cl.sendMsg(msg.chat.id, '❌ Registrazione annullata.');
notifyText = 'Annullata';
return notifyText;
}
// SI alla registrazione standard
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION) {
// set verified (come da tuo codice)
await User.setVerifiedReg(idapp, data.username, data.userDest);
await cl.sendMsg(msg.chat.id, '✅ Registrazione confermata.');
await telegrambot.sendMsgTelegramToTheAdminAllSites(`🆕 Nuova registrazione confermata: ${data.userDest}`);
notifyText = 'Registrazione OK';
return notifyText;
}
// SI/NO alla REGISTRATION_FRIEND
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION_FRIEND) {
await User.setVerifiedReg(idapp, data.username, data.userDest);
await cl.sendMsg(msg.chat.id, '🤝 Conferma amicizia completata!');
notifyText = 'Amicizia OK';
return notifyText;
}
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION_FRIEND) {
await cl.sendMsg(msg.chat.id, '🚫 Invito amicizia rifiutato.');
notifyText = 'Rifiutata';
return notifyText;
}
// deleghe future (es. REGISTRATION_TOZOOM gestita in zoomHandler)
return 'OK';
}
module.exports = { handleRegistration, InlineConferma };

View File

@@ -1,15 +0,0 @@
const { sendMessage, sendPhoto } = require('../api');
const { formatUser, safeExec } = require('../helpers');
const notifyUser = safeExec(async (user, text) => {
if (!user?.telegram_id) return;
const msg = `👋 Ciao ${formatUser(user)}\n${text}`;
await sendMessage(user.telegram_id, msg);
});
const sendUserPhoto = safeExec(async (user, photoUrl, caption) => {
if (!user?.telegram_id) return;
await sendPhoto(user.telegram_id, photoUrl, caption);
});
module.exports = { notifyUser, sendUserPhoto };

View File

@@ -1,32 +0,0 @@
const { User } = require('../../models/user');
async function getTelegramUsersByQuery(query = {}) {
return User.find({
...query,
telegram_id: { $exists: true, $ne: null },
}).lean();
}
async function getAdminTelegramUsers(idapp) {
return getTelegramUsersByQuery({ idapp, isAdmin: true });
}
async function getManagersTelegramUsers(idapp) {
return getTelegramUsersByQuery({ idapp, isManager: true });
}
async function getFacilitatoriTelegramUsers(idapp) {
return getTelegramUsersByQuery({ idapp, isFacilitatore: true });
}
async function getAllTelegramUsersByApp(idapp) {
return getTelegramUsersByQuery({ idapp });
}
module.exports = {
getTelegramUsersByQuery,
getAdminTelegramUsers,
getManagersTelegramUsers,
getFacilitatoriTelegramUsers,
getAllTelegramUsersByApp,
};

View File

@@ -1,27 +0,0 @@
// telegram/handlers/zoomHandler.js
const shared_consts = require('../../tools/shared_nodejs');
const { User } = require('../../models/user');
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
async function handleZoom({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
let notifyText = '';
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION_TOZOOM) {
// nelle tue callback originale: conferma registrazione + messaggio
await User.setVerifiedReg(idapp, data.username, data.userDest);
await cl.sendMsg(msg.chat.id, '🟢 Accesso Zoom confermato!');
notifyText = 'Zoom OK';
return notifyText;
}
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION_TOZOOM) {
await cl.sendMsg(msg.chat.id, '🚫 Accesso Zoom rifiutato.');
notifyText = 'Rifiutato';
return notifyText;
}
return 'OK';
}
module.exports = { handleZoom };

View File

@@ -1,31 +0,0 @@
function formatUser(user) {
const u = user || {};
const username = u.username || (u.profile && u.profile.username_telegram) || 'no_username';
return `${u.name || ''} ${u.surname || ''} (@${username})`.trim();
}
function safeExec(fn) {
return async (...args) => {
try {
return await fn(...args);
} catch (e) {
console.error('Telegram helper error:', e);
return null;
}
};
}
function ensureArray(x) {
if (!x) return [];
return Array.isArray(x) ? x : [x];
}
// utility semplice per evitare flood (se ti serve rate-limit: usa bottleneck)
async function eachSeries(arr, fn) {
for (const item of arr) {
// eslint-disable-next-line no-await-in-loop
await fn(item);
}
}
module.exports = { formatUser, safeExec, ensureArray, eachSeries };

View File

@@ -1,66 +0,0 @@
const messages = require('./messages');
// base api/handlers già creati in precedenza
const { sendMessage, sendPhoto } = require('./api');
const { sendToAdmins } = require('./handlers/adminHandler');
const { notifyUser, sendUserPhoto } = require('./handlers/userHandler');
const { sendNotification } = require('./handlers/notificationHandler');
const { reportError } = require('./handlers/errorHandler');
// NUOVI HANDLER aggiunti ora
const {
sendMsgTelegram,
sendMsgTelegramByIdTelegram,
sendPhotoTelegram,
} = require('./handlers/directHandler');
const {
sendMsgTelegramToTheAdminAllSites,
sendMsgTelegramByIdApp,
sendMsgTelegramToTheManagers,
sendMsgTelegramToTheAdmin,
sendMsgTelegramToTheGroup,
} = require('./handlers/multiAppHandler');
const {
notifyToTelegram,
askConfirmationUser,
sendNotifToAdmin,
sendNotifToManager,
sendNotifToAdminOrManager,
phase,
} = require('./handlers/phaseHandler');
module.exports = {
// messaggi/template
messages,
phase,
// API raw
sendMessage,
sendPhoto,
// generico
sendToAdmins,
notifyUser,
sendUserPhoto,
sendNotification,
reportError,
// EQUIVALENTI del vecchio file
sendMsgTelegram, // (user, text)
sendMsgTelegramByIdTelegram, // (telegramId, text)
sendPhotoTelegram, // (chatIdOrUser, photoUrl, caption)
sendMsgTelegramToTheAdminAllSites, // (text, alsoGroups?)
sendMsgTelegramByIdApp, // (idapp, text)
sendMsgTelegramToTheManagers, // (idapp, text)
sendMsgTelegramToTheAdmin, // (idapp, text)
sendMsgTelegramToTheGroup, // (chatId, text)
notifyToTelegram, // (phase, locals)
askConfirmationUser, // (idapp, phase, user)
sendNotifToAdmin, // (idapp, title, body)
sendNotifToManager, // (idapp, title, body)
sendNotifToAdminOrManager, // (idapp, title, body, preferManagers?)
};

View File

@@ -1,25 +0,0 @@
module.exports = {
// messaggi generici
serverStarted: (dbName) => `🚀 Il server <b>${dbName}</b> è stato avviato con successo.`,
userUnlocked: (user) => `⚠️ L'utente <b>${user.username}</b> (${user.name} ${user.surname}) è stato sbloccato.`,
errorOccurred: (context, err) =>
`❌ Errore in <b>${context}</b>\n<code>${(err && err.message) || err}</code>`,
notifyAdmin: (msg) => `📢 Notifica Admin:\n${msg}`,
// fasi logiche
byPhase: {
REGISTRATION: (locals = {}) =>
`🆕 Nuova registrazione su <b>${locals.nomeapp || 'App'}</b>\nUtente: <b>${locals.username}</b>`,
REGISTRATION_CONFIRMED: (locals = {}) =>
`✅ Registrazione confermata su <b>${locals.nomeapp || 'App'}</b> da <b>${locals.username}</b>`,
RESET_PWD: (locals = {}) =>
`🔁 Reset password richiesto per <b>${locals.username}</b>`,
NOTIFICATION: (locals = {}) =>
`🔔 Notifica: ${locals.text || ''}`,
GENERIC: (locals = {}) =>
`${locals.text || ''}`,
},
askConfirmationUser: (locals = {}) =>
`👋 Ciao <b>${locals.username}</b>!\nConfermi l'operazione su <b>${locals.nomeapp || 'App'}</b>?`,
};

View File

@@ -1 +0,0 @@
http://localhost:8084/signup/paoloar77/SuryaArena/5356627050

View File

@@ -1,488 +0,0 @@
/**
* ======================================================
* TELEGRAM BOT INIT (derived from telegrambot_OLD.js)
* ======================================================
* - Gestione multi-bot per app (arrTelegram)
* - Classe Telegram con funzioni core
* - Invio messaggi, immagini, notifiche
* - Callback Query, menu e inline keyboard
* - API pubbliche per admin, manager, utenti
* - Compatibile con tools, User, Circuit, ecc.
* ======================================================
*/
const TelegramBot = require('node-telegram-bot-api');
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const sharp = require('sharp');
const tools = require('../tools/general');
const shared_consts = require('../tools/shared_nodejs');
const server_constants = require('../tools/server_constants');
const { Site } = require('../models/site');
const { handleCallback } = require('./handlers/callbackHandler');
// -----------------------------------------------------------------------------
// 🔹 COSTANTI, ENUM, EMOJI E TEXT
// -----------------------------------------------------------------------------
const emo = {
JOY: '😂',
JOY2: '🤣',
DANCER: '💃',
STARS: '✨',
FIRE: '🔥',
SUN: '☀️',
TV: '📺',
NEWSPAPER: '🗞',
KISS: '😘',
PENCIL: '✏️',
DREAM: '🏖',
EYES: '😜',
DIZZY: '💫',
ONE_HUNDRED: '💯',
SMILE_STAR: '🤩', // Star-struck
LEFT_FACING_FIST: '🤛', // Left-facing fist
CHECK_VERDE: '✅', // White check mark (verde)
CHECK_GRIGIA: '☑️', // Ballot box with check (grigia)
CROSS_ROSSA: '❌', // X (rossa)
ENVELOPE: '✉️', // Envelope
EXCLAMATION_MARK: '❗', // Exclamation mark
QUESTION_MARK: '❓', // Question mark
ARROW_RIGHT: '➡️', // Arrow pointing to the right
INVITATI: '',
HEART: '❤️',
BLUE_HEART: '💙',
GREEN_HEART: '💚',
YELLOW_HEART: '💛',
PURPLE_HEART: '💜',
GIFT_HEART: '💝',
GIFT: '🎁',
ROBOT_FACE: '🤖',
ADMIN: '💁',
MALE: '💁‍♂️',
FEMALE: '👩‍🦱',
INNOCENT: '😇',
CREDIT_CARD: '💳',
PERSON: '🧑',
};
MsgBot = {
OK: ['si', 'ok'],
CUORE: ['❤️', '💚️', '💜'],
CIAO: ['ciao', 'ciaoo', 'hola', 'holaa', 'hey', 'salve', 'buongiorno', 'buondi', 'ciao ❤️'],
CI_SEI: ['ci sei', "c'è qualcuno", "c'è nessuno"],
CHI_SONO_IO: ['chi sono io', 'chi sono'],
COME_STAI: ['tutto bene', 'come stai', 'come stai', 'come va', 'come butta', 'come va oggi'],
COME_TI_CHIAMI: [
'chi sei',
'come ti chiami',
"qual'è il tuo nome",
"qual'e' il tuo nome",
'che lavoro fai',
'di cosa ti occupi',
],
COSA_FAI: ['cosa fai', 'cosa combini', 'che fai'],
QUANTI_ANNI_HAI: ['quanti anni hai', 'che età hai'],
SEI_LIBERO_STASERA: [
'sei libera stasera',
'sei libero stasera',
'usciamo insieme',
"fare l'amore con me",
'fare sesso',
'vuoi scopare',
'vuoi trombare',
],
MI_TROVI_UN_MOROSO: [
'trovi un moroso',
'una morosa',
'fidanzato',
'fidanzata',
'trovi un marito',
'trovi una moglie',
],
CHAT_EMPOWER: ['chat empower'],
MASCHIO_FEMMINA: ['sei uomo o donna', 'sei maschio o femmina', 'sei ragazzo o ragazza', 'che sesso hai'],
DAMMI_UN_BACIO: ['dammi un bacio', 'baciami'],
HAHA: ['hahaha', 'ahah', '😂'],
MI_AMI: ['mi ami'],
TI_AMO: ['ti amo', 'ti adoro', 'ti lovvo'],
PREGO: ['prego', 'Prego ! 💋💋💋'],
GRAZIE: ['grazie ainy', 'grazie', 'grazie mille', 'graziee', 'grazie ❤', 'grazie❤', 'grazie 😘', 'grazie😘'],
PRINCIPE_AZZURRO: ['principe azzurro'],
START_INV: ['/start inv'],
COSE_COVID: ["cos'è il covid", 'cosa è il covid'],
COVID: ['covid'],
SPOSAMI: ['sposami', 'vuoi sposar', 'sei sposat', 'ci sposiamo', 'ti sposo', 'sei sposat', 'mi sposi'],
CHE_TEMPO_FA: ['che tempo'],
NON_TROO_INVITATI: ['non trovo invitati', 'non riesco a trovare invitati'],
TROVAMI_UN_UOMO_DONNA: ['trovami un uomo', 'trovami una donna', 'esiste una donna per me', 'esiste un uomo per me'],
PAROLACCE: ['stronz', 'fanculo', 'fottiti', 'cagare', 'ammazzat', 'muori', 'cretino', 'stupido'],
COME_SI_CHIAMA: ['come si chiama'],
PROSSIMO_ZOOM: ['prossimo zoom', 'fare lo zoom', 'gli zoom', 'conferenz', 'zoom'],
LAVAGNA: ['lavagna', 'Lavagna', 'LAVAGNA'],
SEI_LIBERO_DI_RESPIRARE: ['sei libero di respirare'],
SEI_LIBERO: ['sei liber', 'sei sposat', 'sei fidanzat', 'sei single'],
AIUTO: [
'help',
'aiuto',
'ho bisogno di',
'ho problemi',
'non riesco',
'mi puoi aiutare',
'mi aiuti',
'aiutami',
'posso chiederti',
'puoi aiutarmi',
],
UOMO: ['uomo', 'maschio'],
SORPRESA: ['noo', 'davvero', 'sii', 'facciamo festa', 'è qui la festa', 'festa'],
UGUALE: ['👍🏻', '✨', '❤🏻', '⭐', '❤', '❤❤', '🤩'],
SI: ['si', 'yes'],
NO: ['no', 'noo'],
DONNA: ['donna', 'femmina'],
FARE_DOMANDA: ['fare una domanda', 'posso farti una domanda'],
DIVENTERO_RICCA: ['diventerò ricc'],
DOVE_VUOI_VIVERE: ['dove vuoi vivere'],
MA_ALLORA: ['ma allora'],
};
const MsgRisp = {
CHAT_EMPOWER:
'Entra nella Chat EMPOWER !!!\n' +
'https://t.me/joinchat/C741mkx5QYXu-kyYCYvA8g ' +
emo.PURPLE_HEART +
emo.GIFT_HEART +
emo.BLUE_HEART,
};
function getemojibynumber(number) {
if (number === 0) {
return '0⃣'; // zero
} else if (number === 1) {
return '1⃣'; // one
} else if (number === 2) {
return '2⃣'; // two
} else if (number === 3) {
return '3⃣'; // three
} else if (number === 4) {
return '4⃣'; // four
} else if (number === 5) {
return '5⃣'; // five
} else if (number === 6) {
return '6⃣'; // six
} else if (number === 7) {
return '7⃣'; // seven
} else if (number === 8) {
return '8⃣'; // eight
} else if (number === 9) {
return '9⃣'; // nine
} else {
return number;
}
}
const Menu = {
LANG_IT: '🇮🇹 Italiano', // Bandiera italiana
LANG_EN: '🇬🇧 English', // Bandiera del Regno Unito
LANG_ES: '🇪🇸 Español', // Bandiera spagnola
LANG_FR: '🇫🇷 Français', // Bandiera francese
LANG_SI: '🇸🇮 Slovenski', // Bandiera slovena
LANG_PT: '🇵🇹 Português', // Bandiera portoghese
LANG: '🌐 Language', // Globo con meridiani
CHAT_PERSONALE: '👩‍💼💻', // Donna impiegata + computer
EXIT_TELEGRAM: 'exittotelegram',
MSG_TO_USER: 'sendmsgto',
ADMIN: '💁‍♀️ Admin', // Persona al banco informazioni
AIUTO: '🔮 Help', // Cristallo magico
ALTRO: '📰 Altro', // Giornale
SETPICPROFILE: '🖼 SetPicProfile', // Cornice con foto
RESETPWD: '🔑 SetResetPwd', // Chiave
MSG_SI_INVITATI_NO_7REQ_INVITATI: '📩Inv e NO 7 Req', // Busta
MSGSTAFF: '📩 Invia a STAFF', // Busta
MSGAPPARTIENE_CIRCUITI_RIS: 'Invia a Utenti dei Circuiti RIS',
MSGPAOLO: '📩 Invia a SURYA', // Busta
RESTART_SRV: '📩Restart-NodeJs', // Busta
REBOOT_SRV: '📩Reboot-VPS!', // Busta
EXECSH: '📩ExecSH', // Busta
LOG_SRV: '🖥Logserver.sh', // Monitor
MSGATUTTI: '📩 Invia a TUTTI', // Busta
it: {
ACCEDI: '👤 Accedi', // Persona
LAVAGNA: '🕉 Lavagna', // Simbolo Om
LINK_CONDIVIDERE: '🔗 Link da condividere', // Link
ZOOM: ' Zoom (Conferenze)', // Informazione
INFO: ' Informazioni', // Informazione
ASSISTENZA: '👐 Le Chat', // Mani aperte
INDIETRO: '🔙 Indietro', // Freccia indietro
SI: '👍 SI', // Pollice su
NO: '👎 NO', // Pollice giù
ESCI_DA_CHAT: '📩 Esci dalla Conversazione', // Busta
NUOVOSITO: '',
},
es: {
ACCEDI: '👤 Entra',
LAVAGNA: '🕉 Tablero',
LINK_CONDIVIDERE: '🔗 Enlaces para compartir',
ZOOM: ' Zoom (Conferencias)',
INFO: ' Información',
ASSISTENZA: '👐 Chats',
INDIETRO: '🔙 Volver',
SI: '👍 SÍ',
NO: '👎 NO',
ESCI_DA_CHAT: '📩 Salir de la conversación',
},
fr: {
ACCEDI: '👤 Entrez',
LAVAGNA: '🕉 Tableau de bord',
LINK_CONDIVIDERE: '🔗 Liens à partager',
ZOOM: ' Zoom (Conférences)',
INFO: ' Informations',
ASSISTENZA: '👐 Les chats',
INDIETRO: '🔙 Retour',
SI: '👍 OUI',
NO: '👎 NON',
ESCI_DA_CHAT: '📩 Quitter la conversation',
},
si: {
ACCEDI: '👤 Prijava',
LAVAGNA: '🕉 Tabla',
LINK_CONDIVIDERE: '🔗 Link za vpis oseb',
ZOOM: ' Zoom (Konference)',
INFO: ' Informacije',
ASSISTENZA: '👐 jev klepet',
INDIETRO: '🔙 Nazaj',
SI: '👍 DA',
NO: '👎 NE',
ESCI_DA_CHAT: '📩 Zaprite pogovor',
},
pt: {
ACCEDI: '👤 Entre',
LAVAGNA: '🕉 Tablero',
LINK_CONDIVIDERE: '🔗 Links para compartilhar',
ZOOM: ' Zoom (Conferências)',
INFO: ' Informações',
ASSISTENZA: '👐 Chats',
INDIETRO: '🔙 Voltar',
SI: '👍 SIM',
NO: '👎 NÃO',
ESCI_DA_CHAT: '📩 Sair da Conversa',
},
enUs: {
ACCEDI: '👤 Enter',
LAVAGNA: '🕉 DashBoard',
LINK_CONDIVIDERE: '🔗 Link to Share',
ZOOM: ' Zoom (Conference)',
INFO: ' Info',
ASSISTENZA: '👐 Chats',
INDIETRO: '🔙 Back',
SI: '👍 YES',
NO: '👎 NO',
ESCI_DA_CHAT: '📩 Exit to the Conversation',
},
};
const CONTA_SOLO = 'contasolo';
const RICEVI_EMAIL = 'riceviemail';
const NOME_COGNOME = 'nomecognome';
const CHEDI_SE_IMBARCARTI = 'chiedi_se_imbarcarti';
const phase = {
REGISTRATION: 'REGISTRATION',
REGISTRATION_CONFIRMED: 'REGISTRATION_CONFIRMED',
RESET_PWD: 'RESET_PWD',
NOTIFICATION: 'NOTIFICATION',
};
const roles = {
ADMIN: 'ADMIN',
MANAGER: 'MANAGER',
FACILITATORE: 'FACILITATORE',
EDITOR: 'EDITOR',
SOCIO: 'SOCIO',
};
// -----------------------------------------------------------------------------
// 🔹 REGISTRY MULTI-BOT (arrTelegram)
// -----------------------------------------------------------------------------
const arrTelegram = [];
function getclTelegByidapp(idapp) {
const rec = arrTelegram.find((r) => String(r.idapp) === String(idapp));
return rec ? rec.cl : null;
}
function getclTelegBytoken(token) {
const rec = arrTelegram.find((r) => r.cl?.token === token);
return rec ? rec.cl : null;
}
// -----------------------------------------------------------------------------
// 🔹 FUNZIONI LOCALI
// -----------------------------------------------------------------------------
async function local_sendMsgTelegram(idapp, username, text) {
const teleg_id = await User.TelegIdByUsername(idapp, username);
const cl = getclTelegByidapp(idapp);
if (cl && teleg_id) return await cl.sendMsg(teleg_id, text);
return null;
}
async function local_sendMsgTelegramToTheManagers(idapp, text, msg, username_bo) {
const managers = await User.getusersManagers(idapp);
const username = username_bo || msg?.chat?.username;
const fullmsg = `${emo.ROBOT_FACE}: Da ${tools.getNomeCognomeTelegram(msg)} (${username})\n${text}`;
if (managers)
for (const m of managers) {
const cl = getclTelegByidapp(idapp);
if (cl && m.profile?.teleg_id)
await cl.sendMsg(m.profile.teleg_id, fullmsg, undefined, undefined, undefined, undefined, true);
}
return true;
}
// -----------------------------------------------------------------------------
// 🔹 CLASSE TELEGRAM
// -----------------------------------------------------------------------------
class Telegram {
constructor(idapp, bot) {
this.idapp = idapp;
this.bot = bot;
this.token = bot.token;
this.arrUsers = [];
this.lastid = 0;
}
// Invia un messaggio base
async sendMsg(chatId, text, parseMode = 'HTML', MyForm = null, message_id, chat_id, ripr_menuPrec, img) {
if (!chatId) return;
try {
await this.bot.sendMessage(chatId, text, { parse_mode: parseMode });
} catch (err) {
console.error('❌ sendMsg error:', err.message);
}
}
// Invia immagine con fallback
async sendImageToTelegram(chatId, imgPath, caption = '') {
if (!chatId || !imgPath) return;
try {
const buffer = fs.readFileSync(imgPath);
const sharped = await sharp(buffer).resize({ width: 1280 }).toBuffer();
await this.bot.sendPhoto(chatId, sharped, { caption });
} catch (err) {
console.error('⚠️ sendImageToTelegram fallback:', err.message);
try {
await this.bot.sendDocument(chatId, imgPath, { caption });
} catch (e) {
console.error('❌ sendDocument fallback:', e.message);
}
}
}
// Placeholder per ricezione messaggi
async receiveMsg(msg) {
console.log('💬', this.idapp, msg.text);
}
// Placeholder per start
async start(msg) {
await this.sendMsg(msg.chat.id, `${emo.ROBOT_FACE} Benvenuto nel bot ${tools.getNomeAppByIdApp(this.idapp)}!`);
}
// Inline keyboard esempio
getInlineKeyboard(buttons) {
return { reply_markup: { inline_keyboard: buttons } };
}
}
// -----------------------------------------------------------------------------
// 🔹 API PUBBLICHE
// -----------------------------------------------------------------------------
async function sendMsgTelegramToTheAdminAllSites(text, senzaintestazione) {
const apps = await tools.getApps();
for (const app of apps) {
const filled = text.replace('{appname}', tools.getNomeAppByIdApp(app.idapp));
await sendMsgTelegramToTheAdmin(app.idapp, filled, senzaintestazione);
}
}
async function sendMsgTelegramToTheAdmin(idapp, text, senzaintestazione) {
const usersadmin = await User.getusersAdmin(idapp);
let head = emo.ROBOT_FACE + '[BOT-ADMIN]' + emo.ADMIN + ': ';
if (senzaintestazione) head = '';
if (usersadmin)
for (const rec of usersadmin) {
if (User.isAdmin(rec.perm)) {
await sendMsgTelegramByIdTelegram(idapp, rec.profile.teleg_id, head + text);
await tools.snooze(300);
}
}
return true;
}
async function sendMsgTelegramByIdTelegram(idapp, idtelegram, text, message_id, chat_id, ripr_menuPrec, MyForm = null, img = '') {
if (!idtelegram) return;
const cl = getclTelegByidapp(idapp);
if (cl) return await cl.sendMsg(idtelegram, text, null, MyForm, message_id, chat_id, ripr_menuPrec, img, { idapp });
}
async function sendMsgTelegramToTheManagers(idapp, text) {
const managers = await User.getusersManagers(idapp);
if (managers)
for (const rec of managers) {
await sendMsgTelegramByIdTelegram(idapp, rec.profile.teleg_id, text);
await tools.snooze(300);
}
return true;
}
// -----------------------------------------------------------------------------
// 🔹 CALLBACK QUERY HANDLER (semplificata ma compatibile)
// -----------------------------------------------------------------------------
function setupCallback(bot, cl) {
bot.on('callback_query', async (callbackQuery) => {
await handleCallback(bot, cl, callbackQuery);
});
}
// -----------------------------------------------------------------------------
// 🔹 INIZIALIZZAZIONE BOT MULTI-APP
// -----------------------------------------------------------------------------
async function initTelegramBots() {
const arrApps = await tools.getApps();
for (const app of arrApps) {
const idapp = app.idapp;
const token = tools.getTelegramKeyByIdApp(idapp);
const nomebot = tools.getTelegramBotNameByIdApp(idapp);
if (!token) continue;
console.log(`🤖 Avvio BOT: ${nomebot} (${idapp})`);
const bot = new TelegramBot(token, { polling: true });
const cl = new Telegram(idapp, bot);
arrTelegram.push({ idapp, cl });
bot.onText(/\/start/, (msg) => cl.start(msg));
bot.on('message', async (msg) => cl.receiveMsg(msg));
setupCallback(bot, cl);
}
}
// -----------------------------------------------------------------------------
// 🔹 ESPORTAZIONI PUBBLICHE
// -----------------------------------------------------------------------------
module.exports = {
Telegram,
initTelegramBots,
getclTelegByidapp,
getclTelegBytoken,
sendMsgTelegramToTheAdminAllSites,
sendMsgTelegramToTheAdmin,
sendMsgTelegramByIdTelegram,
sendMsgTelegramToTheManagers,
local_sendMsgTelegram,
local_sendMsgTelegramToTheManagers,
phase,
roles,
emo,
};

View File

@@ -1330,5 +1330,6 @@ module.exports = {
JOB_TO_EXECUTE: {
MIGRATION_SECTORS_DIC25: 'Migration_Sectors_Dic_2025',
MIGRATION_TELEGRAM_30DIC25: 'Migration_Telegram_30Dic_2025',
},
};