From 77b04e22ca159c5472bb4310bcdd516d60936d9f Mon Sep 17 00:00:00 2001 From: Jesus0204 Date: Sat, 28 Sep 2024 23:53:26 -0600 Subject: [PATCH 1/3] feature: back para enviar notificaciones en batches a usuarios --- models/perfil/usuario.model.js | 5 ++ .../registrarAnuncio.controller.js | 22 ++++++ .../controllers/actualizarFCM.controller.js | 39 ++++++++++ .../session/routes/actualizarFCM.routes.js | 10 +++ modules/session/routes/sessionIndex.routes.js | 2 + util/sendNotification.js | 78 +++++++++++++++++++ 6 files changed, 156 insertions(+) create mode 100644 modules/session/controllers/actualizarFCM.controller.js create mode 100644 modules/session/routes/actualizarFCM.routes.js create mode 100644 util/sendNotification.js diff --git a/models/perfil/usuario.model.js b/models/perfil/usuario.model.js index 1218de4..848eb69 100644 --- a/models/perfil/usuario.model.js +++ b/models/perfil/usuario.model.js @@ -56,6 +56,11 @@ const usuarioSchema = new mongoose.Schema( unique: true, required: true }, + fcmTokens: { + type: [String], + default: [], + required: false + } }, { collection: "Usuario", diff --git a/modules/anuncios/controllers/registrarAnuncio.controller.js b/modules/anuncios/controllers/registrarAnuncio.controller.js index 980f4b1..e3d1e19 100644 --- a/modules/anuncios/controllers/registrarAnuncio.controller.js +++ b/modules/anuncios/controllers/registrarAnuncio.controller.js @@ -1,5 +1,10 @@ const Announcement = require('../../../models/otros/anuncio.model'); +const { + Usuario +} = require('../../../models/perfil/usuario.model'); + +const sendNotification = require('../../../util/sendNotification'); exports.postAnnouncement = async (request, response) => { const firebaseUID = request.userUID.uid; @@ -8,6 +13,23 @@ exports.postAnnouncement = async (request, response) => { const imagen = request.body.imagen; try { await Announcement.postAnnouncement(firebaseUID, titulo, contenido, imagen); + + // Obtener todos los tokens de FCM de los usuarios + const usuarios = await Usuario.find({ + fcmTokens: { + $exists: true, + $ne: [] + } + }); + const tokens = usuarios.reduce((acc, usuario) => acc.concat(usuario.fcmTokens), []); + + // Definir el contenido de la notificación + const tituloNotificacion = titulo; + const cuerpoNotificacion = contenido; + + // Enviar la notificación + await sendNotification(tokens, tituloNotificacion, cuerpoNotificacion); + return response.status(201).json({ message: 'Anuncio creado correctamente' }); } catch (error) { return response.status(500).json({ message: 'Error al crear el anuncio', error: error.message }); diff --git a/modules/session/controllers/actualizarFCM.controller.js b/modules/session/controllers/actualizarFCM.controller.js new file mode 100644 index 0000000..9a71b05 --- /dev/null +++ b/modules/session/controllers/actualizarFCM.controller.js @@ -0,0 +1,39 @@ +const { + Usuario +} = require('../../../models/perfil/usuario.model'); + +exports.actualizarFCM = async (request, response) => { + const { + fcmToken + } = request.body; + + if (!fcmToken) { + return response.status(400).json({ + message: 'fcmToken es requerido' + }); + } + + try { + // Buscar al usuario por UID + const user = await Usuario.findOne({ + firebaseUID: request.userUID.uid + }); + + if (user) { + // Agregar el token si no existe + if (!user.fcmTokens.includes(fcmToken)) { + user.fcmTokens.push(fcmToken); + await user.save(); + } + } + + response.status(200).json({ + message: 'Token de FCM actualizado exitosamente' + }); + } catch (error) { + console.error('Error al actualizar el token de FCM:', error); + response.status(500).json({ + message: 'Error al actualizar el token de FCM' + }); + } +} diff --git a/modules/session/routes/actualizarFCM.routes.js b/modules/session/routes/actualizarFCM.routes.js new file mode 100644 index 0000000..8edcdb7 --- /dev/null +++ b/modules/session/routes/actualizarFCM.routes.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); + +const verifyToken = require('../../../util/verifyUserToken') + +const actualizarFCMController = require('../controllers/actualizarFCM.controller'); + +router.post('/', verifyToken, actualizarFCMController.actualizarFCM); + +module.exports = router; \ No newline at end of file diff --git a/modules/session/routes/sessionIndex.routes.js b/modules/session/routes/sessionIndex.routes.js index b22f8cf..0995499 100644 --- a/modules/session/routes/sessionIndex.routes.js +++ b/modules/session/routes/sessionIndex.routes.js @@ -6,11 +6,13 @@ const rolPrivilegioRoute = require('./rolPrivilegio.routes'); const getUserEmailRoute = require('./getUserEmail.routes'); const perfilCompletoRoute = require('./perfilCompleto.routes'); const getUsernameRoute = require('./getUsername.routes'); +const actualizarFCMRoute = require('./actualizarFCM.routes'); router.use('/registrarUsuario', registrarUsuarioRoute); router.use('/rolPrivilegio', rolPrivilegioRoute); router.use('/getUserEmail', getUserEmailRoute); router.use('/perfilCompleto', perfilCompletoRoute); router.use('/getUsername', getUsernameRoute); +router.use('/actualizarTokenNotificacion', actualizarFCMRoute); module.exports = router; \ No newline at end of file diff --git a/util/sendNotification.js b/util/sendNotification.js new file mode 100644 index 0000000..909a1dd --- /dev/null +++ b/util/sendNotification.js @@ -0,0 +1,78 @@ +const admin = require('./firebase-admin-config'); +const { + Usuario +} = require('../models/perfil/usuario.model'); + +const chunkArray = (array, size) => { + const result = []; + for (let i = 0; i < array.length; i += size) { + result.push(array.slice(i, i + size)); + } + return result; +}; + +const sendNotification = async (tokens, titulo, cuerpo, data = {}) => { + if (!tokens || tokens.length === 0) { + console.log('No hay tokens de FCM para enviar notificaciones.'); + return; + } + + // Dividir los tokens en grupos de 500 como máximo + const tokenChunks = chunkArray(tokens, 500); + + try { + for (const chunk of tokenChunks) { + const responses = await admin.messaging().sendEachForMulticast({ + tokens: chunk, + notification: { + title: titulo, + body: cuerpo, + }, + data: data + }); + + let successCount = 0; + let failureCount = 0; + const failedTokens = []; + + responses.responses.forEach((response, index) => { + if (response.success) { + successCount++; + } else { + failureCount++; + const failedToken = chunk[index]; + console.error(`Error al enviar a ${failedToken}: ${response.error.message}`); + if ( + response.error.code === 'messaging/invalid-registration-token' || + response.error.code === 'messaging/registration-token-not-registered' + ) { + failedTokens.push(failedToken); + } + } + }); + + console.log(`${successCount} mensajes enviados con éxito`); + console.log(`${failureCount} mensajes fallaron`); + + // Si hay tokens fallidos, eliminarlos de la base de datos + if (failedTokens.length > 0) { + await Usuario.updateMany({ + fcmTokens: { + $in: failedTokens + } + }, { + $pull: { + fcmTokens: { + $in: failedTokens + } + } + }); + console.log('Tokens inválidos eliminados de la base de datos.'); + } + } + } catch (error) { + console.error('Error al enviar notificaciones:', error); + } +}; + +module.exports = sendNotification; \ No newline at end of file From daad892b7c4f1a3439e7a51702bde54a4b505bf6 Mon Sep 17 00:00:00 2001 From: Jesus0204 Date: Sun, 29 Sep 2024 10:23:56 -0600 Subject: [PATCH 2/3] fix: Enviar anuncioID para que al tocar notifiacion los lleve a tab de anuncios --- models/otros/anuncio.model.js | 1 + modules/anuncios/controllers/registrarAnuncio.controller.js | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/models/otros/anuncio.model.js b/models/otros/anuncio.model.js index b592000..77dedce 100644 --- a/models/otros/anuncio.model.js +++ b/models/otros/anuncio.model.js @@ -42,6 +42,7 @@ async function postAnnouncement(firebaseUID, titulo, contenido, imagen){ imagen: imagen }); await announcement.save(); + return announcement; } async function getAnnouncements(){ diff --git a/modules/anuncios/controllers/registrarAnuncio.controller.js b/modules/anuncios/controllers/registrarAnuncio.controller.js index e3d1e19..97d36a5 100644 --- a/modules/anuncios/controllers/registrarAnuncio.controller.js +++ b/modules/anuncios/controllers/registrarAnuncio.controller.js @@ -12,7 +12,7 @@ exports.postAnnouncement = async (request, response) => { const contenido = request.body.contenido; const imagen = request.body.imagen; try { - await Announcement.postAnnouncement(firebaseUID, titulo, contenido, imagen); + const announcement = await Announcement.postAnnouncement(firebaseUID, titulo, contenido, imagen); // Obtener todos los tokens de FCM de los usuarios const usuarios = await Usuario.find({ @@ -28,7 +28,9 @@ exports.postAnnouncement = async (request, response) => { const cuerpoNotificacion = contenido; // Enviar la notificación - await sendNotification(tokens, tituloNotificacion, cuerpoNotificacion); + await sendNotification(tokens, tituloNotificacion, cuerpoNotificacion, { + anuncioID: announcement._id.toString(), + }); return response.status(201).json({ message: 'Anuncio creado correctamente' }); } catch (error) { From c790d83c458fe9c87a0f602596e9c39e3081e305 Mon Sep 17 00:00:00 2001 From: Jesus0204 Date: Sun, 29 Sep 2024 10:34:53 -0600 Subject: [PATCH 3/3] fix: asegurar que solo haya un token por dispositivo en la base de datos --- modules/session/controllers/actualizarFCM.controller.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/session/controllers/actualizarFCM.controller.js b/modules/session/controllers/actualizarFCM.controller.js index 9a71b05..0dac027 100644 --- a/modules/session/controllers/actualizarFCM.controller.js +++ b/modules/session/controllers/actualizarFCM.controller.js @@ -14,6 +14,15 @@ exports.actualizarFCM = async (request, response) => { } try { + // Eliminar el token de cualquier otra cuenta que lo tenga registrado + await Usuario.updateMany({ + fcmTokens: fcmToken + }, { + $pull: { + fcmTokens: fcmToken + } + }); + // Buscar al usuario por UID const user = await Usuario.findOne({ firebaseUID: request.userUID.uid