diff --git a/core/passport.js b/core/passport.js index a80c5c5..8a247e8 100644 --- a/core/passport.js +++ b/core/passport.js @@ -116,12 +116,9 @@ passport.use('jwt', new CustomStrategy(async (req, done) => { let user = await authService.extraMethods.findUser({ id: result.id }); if (user) { user = user.toJSON(); - if (appVersion) { - if (user.app_version != appVersion){ - const result = userService._updateLastLoginAndVersionUser(user.id, appVersion); - user.app_version = appVersion; - } - } + const result = userService._updateLastLoginAndVersionUser(user.id, appVersion); + user.app_version = appVersion; + delete user.password; console.log('Logged in Successfully'); return done(null, user, { message: 'Logged in Successfully' }); diff --git a/helpers/composes.helper.js b/helpers/composes.helper.js index e8fcff4..9c04b46 100644 --- a/helpers/composes.helper.js +++ b/helpers/composes.helper.js @@ -258,9 +258,7 @@ const usersIdsComposer = (inscriptions) => { let usersId = [] if (inscriptions) { inscriptions.map((inscription) => { - usersId.push({ - userId: inscription.userId, - }); + usersId.push(inscription.userId); }); }; return usersId; diff --git a/helpers/message.helper.js b/helpers/message.helper.js index d8a718a..4fb0a84 100644 --- a/helpers/message.helper.js +++ b/helpers/message.helper.js @@ -1,5 +1,7 @@ 'use strict'; +const tinytim = require('tinytim').tim; + //////////////////////////////////////////////////////////////////////////////// // CONSTANTS //////////////////////////////////////////////////////////////////////////////// @@ -29,7 +31,16 @@ function buildMessage(text) { return buildGenericMessage(TITLE_MESSAGE, text) } +function tinytom(literal, data) { + if (typeof literal === 'object' && literal !== null) { + return JSON.parse(tinytim(JSON.stringify(literal), data)); + } else { + return tinytim(literal, data); + } +} + module.exports = { + tinytom, buildErrorMessage, buildMessage, //For testing diff --git a/helpers/messages.json b/helpers/messages.json index 7f7b020..e7482c1 100644 --- a/helpers/messages.json +++ b/helpers/messages.json @@ -21,5 +21,21 @@ "html": "¡Hola!,

Solicitaste cambiar la contraseña, hemos creado una nueva.

Entra en la app con tu dirección de email y esta clave: {{password}}

Fundación Lo Que De Verdad Importa." } } + }, + "push": { + "confirmInvitation": { + "title": "{{ congress }}", + "body": "Tu inscripción al congreso de {{ congress }} está confirmada.", + "data": { + "type": "message", + "title": "{{ congress }}", + "message": "Tu inscripción al congreso de {{ congress }} está confirmada.\nPor favor, si no puedes asistir al congreso, elimina tu inscripción para que otra persona pueda asistir.\n", + "button": { + "caption": "Ver mi entrada", + "screen": "Ticket", + "paramId": "{{ ticketId }}" + } + } + } } } \ No newline at end of file diff --git a/helpers/notification.helpers.js b/helpers/notification.helpers.js new file mode 100644 index 0000000..c61abc7 --- /dev/null +++ b/helpers/notification.helpers.js @@ -0,0 +1,44 @@ +const moment = require('moment'); +const { tinytom } = require('./message.helper'); +const messages = require('./messages.json'); + + +const createNotification = (data) => { + return { + date: data.date, + title: data.title, + body: data.body, + ttl: data.ttl, + priority: data.priority ? data.priority : 'high', + recipients: data.recipients, + data: data.data, + userId: data.userId, + } +} + +createNotificationValidatedInscription = (inscription) => { + let jsonMessage = messages.push.confirmInvitation; + + const jsonNotification = tinytom(jsonMessage, { + congress: inscription.event.name, + ticketId: inscription.id, + }); + + return { + ...jsonNotification, + date: moment(), + priority: "high", + recipients: { + "userIds": [ + inscription.user.id, + ] + }, + } +} + + +module.exports = { + createNotificationValidatedInscription, + createNotification, + //createMessageNotification +} diff --git a/helpers/push.helper.js b/helpers/push.helper.js new file mode 100644 index 0000000..f992ac1 --- /dev/null +++ b/helpers/push.helper.js @@ -0,0 +1,133 @@ +const { Expo } = require('expo-server-sdk'); + +// Create a new Expo SDK client +const expo = new Expo(); + +const createPushMessage = (data) => { + const pushMessage = { + title: data.title, + body: data.body, + ttl: data.ttl, + priority: data.priority, + userId: data.userId, + to: data.to, + notificationId: data.id, + data: data.data, + _displayInForeground: true, + sound: 'default', + channelId: 'lqdvi-messages' + }; + console.log(pushMessage); + return pushMessage; +}; + + + + + +const sendPushMessage = async (messages) => { + + // The Expo push notification service accepts batches of notifications so + // that you don't need to send 1000 requests to send 1000 notifications. We + // recommend you batch your notifications to reduce the number of requests + // and to compress them (notifications with similar content will get + // compressed). + + /** + * There is a limit on the number of push notifications (100) you can send at once.Use + * `chunkPushNotifications` to divide an array of push notification messages into appropriately + * sized chunks + */ + + // Later, after the Expo push notification service has delivered the + // notifications to Apple or Google (usually quickly, but allow the the service + // up to 30 minutes when under load), a "receipt" for each notification is + // created. The receipts will be available for at least a day; stale receipts + // are deleted. + // + // The ID of each receipt is sent back in the response "ticket" for each + // notification. In summary, sending a notification produces a ticket, which + // contains a receipt ID you later use to get the receipt. + // + // The receipts may contain error codes to which you must respond. In + // particular, Apple or Google may block apps that continue to send + // notifications to devices that have blocked notifications or have uninstalled + // your app. Expo does not control this policy and sends back the feedback from + // Apple and Google so you can handle it appropriately. + + let chunks = expo.chunkPushNotifications(messages); + return { + messages, + tickets: await _sendPushNotificationsAsync(chunks) + }; +}; + +module.exports = { + sendPushMessage, + createPushMessage, +} + + +const _sendPushNotificationsAsync = async function (chunks) { + return new Promise(async function (resolve) { + let tickets = []; + // Send the chunks to the Expo push notification service. There are + // different strategies you could use. A simple one is to send one chunk at a + // time, which nicely spreads the load out over time: + for (let chunk of chunks) { + try { + let ticketChunk = await expo.sendPushNotificationsAsync(chunk); + tickets.push(...ticketChunk); + + // NOTE: If a ticket contains an error code in ticket.details.error, you + // must handle it appropriately. The error codes are listed in the Expo + // documentation: + // https://docs.expo.io/versions/latest/guides/push-notifications#response-format + + } catch (error) { + console.error(error); + } + } + + resolve(tickets); + }); +}; + +const _getPushNotificationsResultAsync = async function (receiptIdChunks) { + return new Promise(async function (resolve) { + // Like sending notifications, there are different strategies you could use + // to retrieve batches of receipts from the Expo service. + + let result = []; + + console.log(receiptIdChunks); + + for (let chunk of receiptIdChunks) { + try { + let receipts = await expo.getPushNotificationReceiptsAsync(chunk); + console.log('hola', receipts); + + // The receipts specify whether Apple or Google successfully received the + // notification and information about an error, if one occurred. + for (let key in receipts) { + if (receipts[key].status === 'ok') { + result.push[receipts[key]]; + continue; + } else if (receipts[key].status === 'error') { + console.error(`There was an error sending a notification: ${receipts[key].message}`); + if (receipts[key].details && receipts[key].details.error) { + // The error codes are listed in the Expo documentation: + // https://docs.expo.io/versions/latest/guides/push-notifications#response-format + // You must handle the errors appropriately. + console.error(`The error code is ${receipts[key].details.error}`); + } + } + } + } catch (error) { + console.error(error); + } + } + + resolve(result); + }); +} diff --git a/modules/auth/user.model.js b/modules/auth/user.model.js index 1da9242..0af072e 100644 --- a/modules/auth/user.model.js +++ b/modules/auth/user.model.js @@ -77,7 +77,7 @@ module.exports = function (sequelize, DataTypes) { User.Comments = User.hasMany(models.Comment, { foreignKey: 'userId' }); User.EventsReservations = User.hasMany(models.EventReservation, { foreignKey: 'userId' }); User.EventsInscriptions = User.hasMany(models.EventInscription, { foreignKey: 'userId' }); - User.Questions = User.hasMany(models.EventQuestion, { foreignKey: 'userId', as: "questions", required: false, }); + User.Questions = User.hasMany(models.EventQuestion, { foreignKey: 'userId', as: "questions", required: false }); // User.InscriptionsValidate = User.hasMany(models.EventIncription, { foreignkey: 'validateUserId'}) //User.Reactions = User.hasMany(models.UserReaction, { foreignKey: 'UserId' }); diff --git a/modules/auth/user.service.js b/modules/auth/user.service.js index ea39c22..59823a5 100644 --- a/modules/auth/user.service.js +++ b/modules/auth/user.service.js @@ -26,14 +26,13 @@ const extraMethods = { }) }, - _updateLastLoginAndVersionUser: async (Id, appVersion) => { - return models.User.update ( - { app_version : appVersion, - lastlogin: moment().utc() }, - { - where: { id: Id} - }, - ); + _updateLastLoginAndVersionUser: async (id, appVersion) => { + return models.User.update ({ + app_version : appVersion, + lastlogin: moment().utc() + }, { + where: { id: id } + }); }, _getOrCreateUser: async (dataUser) => { diff --git a/modules/events/event.controller.js b/modules/events/event.controller.js index cc11e0e..ebf5cf1 100644 --- a/modules/events/event.controller.js +++ b/modules/events/event.controller.js @@ -4,11 +4,14 @@ const httpStatus = require('http-status'); const generateControllers = require('../../core/controllers'); const QRHelper = require('../../helpers/qr.helper'); const emailHelper = require('../../helpers/mail.helper'); +const notificationHelper = require('../../helpers/notification.helpers'); const path = require("path"); const messages = require('../../helpers/messages.json'); const eventService = require('./event.service'); const eventReservationService = require('./events_reservations.service'); const eventInscriptionService = require('./events_inscriptions.service'); +const notificationService = require('../notification/notification.service'); + const { extractParamsFromRequest, handleErrorResponse, handleResultResponse } = require('../../helpers/controller.helper'); //PRUEBA @@ -26,25 +29,25 @@ function generateMemberInscription (user, inscription, reservation) { let memberInscription = null; if (user && inscription) { memberInscription = { - marketing_memberId: null, - email: user.email, - name: user.name, - surname: user.surname, - source: inscription.source, - event_name: (inscription.event) ? inscription.event.name : 'N/A', - event_date: (inscription.event) ? inscription.event.init_date : 'N/A', - reservation_code: (reservation) ? reservation.reservation_code : null, - date_inscription: inscription.date, - code_ticket: inscription.code_ticket, - validated: inscription.validated, - color: (reservation) ? reservation.color : null, - description: ((reservation) ? reservation.description : 'Entrada').toUpperCase(), - entity: (reservation) ? reservation.Entity.name : user.entityId, - userId: user.id, - qrConfig: null, - qrCode: null, + marketing_memberId: null, + email: user.email, + name: user.name, + surname: user.surname, + source: inscription.source, + event_name: (inscription.event) ? inscription.event.name : 'N/A', + event_date: (inscription.event) ? inscription.event.init_date : 'N/A', + reservation_code: (reservation) ? reservation.reservation_code : null, + date_inscription: inscription.date, + code_ticket: inscription.code_ticket, + validated: inscription.validated, + color: (reservation) ? reservation.color : null, + description: ((reservation) ? reservation.description : 'Entrada').toUpperCase(), + entity: (reservation) ? reservation.Entity.name : user.entityId, + userId: user.id, + qrConfig: null, + qrCode: null, - } + } } return memberInscription; @@ -264,19 +267,32 @@ console.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ', member); - //Mandar correo de confirmacion de desinscripcion + + //Mandar correo de confirmacion de inscripcion try { + + var member = generateMemberInscription(inscription.user, inscription, inscription.reservation); + member.marketing_memberId = await eventInscriptionService._addMember(marketingListIdEvent, member); + eventInscriptionService._updateMarketingMemberOfInscription(inscription.id, member.marketing_memberId); + member.qrConfig = generateQRConfig(member); + member.qrCode = await QRHelper.getInscriptionQRCode(member.qrConfig); + console.log('mandar correo>>>>>>>>>>>>>>>>>>>>> ', member); emailHelper.sendTicket(generateHeaderMail(member), generateBodyMail(member)) } catch (error) { console.log('No se ha podido mandar email con entrada'); }; + + + try { + let notification = notificationHelper.createNotificationValidatedInscription(inscription); + console.log(notification); + let result = notificationService.sendNotification(notification, [inscription.user.id]); + + console.log(result); + } catch (error) { + console.log('No se ha podido mandar push'); + }; }; return handleResultResponse("Inscripción validada", null, params, res, httpStatus.OK); @@ -285,6 +301,7 @@ console.log('mandar correo>>>>>>>>>>>>>>>>>>>>> ', member); return handleResultResponse("No se pudo validar inscripción", null, params, res, httpStatus.NOT_FOUND); } catch (error) { + console.log(error); return handleResultResponse("Error al validar inscripción", null, params, res, httpStatus.NOT_FOUND); } diff --git a/modules/notification/notification.controller.js b/modules/notification/notification.controller.js index cf84b86..23cbc93 100644 --- a/modules/notification/notification.controller.js +++ b/modules/notification/notification.controller.js @@ -8,6 +8,8 @@ const notificationDetailService = require('./notification_detail.service'); const userDeviceService = require('./user_device.service'); const eventInscriptionService = require('../events/events_inscriptions.service'); const { usersIdsComposer } = require('../../helpers/composes.helper'); +const pushHelper = require('../../helpers/push.helper'); +const notificationHelper = require('../../helpers/notification.helpers') const { extractParamsFromRequest, handleErrorResponse, handleResultResponse } = require('../../helpers/controller.helper'); @@ -17,77 +19,32 @@ const controllerOptions = { MODULE_NAME }; const extraControllers = { - sendNotificationEvent: async (req, res, next) => { - - /** - * notificationSample = { - * "tittle": "título de la notificación", - * "message": "cuerpo de la notificación", - * "recipients": { - * "eventId": "xxx-xxx-xxx-xxx", - * "segment": "ALL" | "ALL_VALIDATED" | "ALL_NOT_VALIDATED" | - * "PARTNERS_ALL" | "PARTNERS_VALIDATED" | "PARTNERS_NOT_VALIDATED" | - * "COLLEGE_ALL" | "COLLEGE_VALIDATED" | "COLLEGE_NOT_VALIDATED" - * }, - * "data": { - * "type": "message", - * "title": "Título del mensaje", - * "message": "Cuerpo del mensaje", - * "button": { - * "caption": "Etiqueta del boton", - * "url": "https://www.site.es", - * "screen": "", - * "paramId": "23", - * } - * } - *} - */ - let usersIds = null; - const params = req.body; - const eventId = params.recipients.eventId; - const segment = params.recipients.segment; - console.log('prueba de llamada>>>>> ', params); - - try { - notificationService.createNotification(params.date, params.title, params.message, undefined, 'default', params.data, req.user.id); - switch (segment) { - //Todos los inscritos al evento tanto en validos como en lista de espera - case 'ALL': - usersIds = await eventInscriptionService._getInscriptionByEvent(eventId); - break; - //Todos los inscritos tanto invitados como libres - case 'ALL_VALIDATED': - usersIds = await eventInscriptionService._getInscriptionByEventAndValidated(eventId, true); - break; - //Todos los de lista de espera tanto invitados como libres (Actualmente en invitados no hay lista de espera) - case 'ALL_NOT_VALIDATED': - usersIds = await eventInscriptionService._getInscriptionByEventAndValidated(eventId, false); - break; - - //Solo invitados como actualmente no se usa codigo de reserva para los coles, vale con filtrar por aquellos que tengan codigo de reserva - case 'PARTNERS_ALL': - usersIds = await eventInscriptionService._getInscriptionByEventFromPartner(eventId); - - break; - - //Todos los inscritos al evento, tanto validados como en lista de espera - default: //ALL - break; - } - - usersIds = usersIdsComposer(usersIds); - console.log('usuarios inscritos>>>>> ', usersIds); - - } catch(error) { - return handleErrorResponse(MODULE_NAME, 'sendNotificationEvent', error, res); - } - - - - return handleResultResponse("sendNotificationEvent", null, params, res, httpStatus.OK); - }, - sendNotification: (config) => { + + /** + * notificationSample = { + * "tittle": "título de la notificación", + * "message": "cuerpo de la notificación", + * "recipients": { + * "eventId": "xxx-xxx-xxx-xxx", + * "segment": "ALL" | "ALL_VALIDATED" | "ALL_NOT_VALIDATED" | + * "PARTNERS_ALL" | "PARTNERS_VALIDATED" | "PARTNERS_NOT_VALIDATED" | + * "COLLEGE_ALL" | "COLLEGE_VALIDATED" | "COLLEGE_NOT_VALIDATED" + * }, + * "data": { + * "type": "message", + * "title": "Título del mensaje", + * "message": "Cuerpo del mensaje", + * "button": { + * "caption": "Etiqueta del boton", + * "url": "https://www.site.es", + * "screen": "", + * "paramId": "23", + * } + * } + *} + */ + return async (req, res, next) => { config = config || { scopes: [], @@ -97,115 +54,70 @@ const extraControllers = { const context = buildContext(req, config); let params = extractParamsFromRequest(req, res); - let userIds = req.body.userIds; - if (!userIds) { - return handleErrorResponse(controllerOptions.MODULE_NAME, 'sendNotification', new Error('Missing user Ids'), res) - } + let userIds = undefined; + let eventId = undefined; + let segment = undefined; + const { body } = req; - if (!req.body.title) { + if (!body.title) { return handleErrorResponse(controllerOptions.MODULE_NAME, 'sendNotification', new Error('Missing message title'), res) } - if (!req.body.message) { - return handleErrorResponse(controllerOptions.MODULE_NAME, 'sendNotification', new Error('Missing message content'), res) + if (!body.body) { + return handleErrorResponse(controllerOptions.MODULE_NAME, 'sendNotification', new Error('Missing body content'), res) } - try { - let getUserDevicesPromise = (userId) => userDeviceService.fetchAll({ params: { userId: userId }}, context).then(function(result) { - return new Promise(function(resolve) { resolve(result.rows) }); - }); - let saveNotificationPromise = (notification) => notificationService.create(notification, context); - let sendNotificationsPromise = (messages) => notificationService.sendNotification(messages); - let disableUserDevicePromise = (token) => userDeviceService.update({ params: { - token: token, - }}, { valid: 0, invalidated: moment() }, context); + // Evento? + if (body.recipients.eventId) { + eventId = body.recipients.eventId; + segment = body.recipients.segment; + } else if (body.recipients.userIds) { + userIds = body.recipients.userIds; + } else { + return handleErrorResponse(controllerOptions.MODULE_NAME, 'sendNotification', new Error('Missing user Ids or event Ids'), res) + } - let disableInvalidUserDevicesPromise = (userDevices) => { - return new Promise(function (resolve) { - let _userDevices = []; - userDevices.forEach(async function (userDevice) { - if (!userDeviceService.isValidPushToken(userDevice.token)) { - await disableUserDevicePromise(userDevice.token); - } else { - _userDevices.push(userDevice); - } - }); - resolve(_userDevices) - }); - }; - - let disableUserDevicesWithErrorStatus = (messages, tickets) => { - return new Promise(function (resolve) { - tickets.forEach(async function (ticket, index) { - if ((ticket.status === 'error') && (ticket.details.error === 'DeviceNotRegistered')) { - await disableUserDevicePromise(messages[index].to) - } - }); - resolve(true); - }); - } - - let saveResponseStatusPromise = (messages, tickets) => notificationDetailService.saveNotificationDetails(messages, tickets); - - const notificationRecord = { - date: moment(), - title: req.body.title, - body: req.body.message, - ttl: req.body.ttl || undefined, - priority: req.body.priority || 'default', - data: req.body.data || { userIds: req.body.userIds }, - userId: context.user.id, - }; - - let buildMessagesPromise = (userDevices) => { - return new Promise(function (resolve) { - let messages = []; - - userDevices.forEach(async function (userDevice) { - messages.push({ - title: notificationRecord.title, - body: notificationRecord.body, - ttl: notificationRecord.ttl, - priority: notificationRecord.priority, - userId: userDevice.userId, - to: userDevice.token, - notificationId: notificationRecord.id, - data: notificationRecord.data, - _displayInForeground: true, - sound: 'default', - }); - }); - - resolve(messages) - }); - }; - - let getUserDevicesList = []; - - userIds.forEach(function (userId) { - getUserDevicesList.push(getUserDevicesPromise(userId)); + try { + let notification = notificationHelper.createNotification ({ + ...body, + userId: context.user.id }); - saveNotificationPromise(notificationRecord) - .then(function(notification) { - notificationRecord.id = notification.id; - return Promise.all(getUserDevicesList) - }).then(function (userDeviceList) { - return new Promise(function(resolve) { resolve(userDeviceList[0]); }); - }) - .then(disableInvalidUserDevicesPromise) - .then(buildMessagesPromise) - .then(sendNotificationsPromise) - .then(function({ messages, tickets }) { - let _saveStatus = saveResponseStatusPromise(messages, tickets); - let _disableDevices = disableUserDevicesWithErrorStatus(messages, tickets); + let getUserIds = async () => { + if (userIds) { + return new Promise(function(resolve) { resolve(userIds) } ); + } else if (eventId && segment) { + switch (segment) { + //Todos los inscritos tanto invitados como libres + case 'ALL_VALIDATED': + userIds = await eventInscriptionService._getInscriptionByEventAndValidated(eventId, true); + break; + //Todos los de lista de espera tanto invitados como libres (Actualmente en invitados no hay lista de espera) + case 'ALL_NOT_VALIDATED': + userIds = await eventInscriptionService._getInscriptionByEventAndValidated(eventId, false); + break; + + //Solo invitados como actualmente no se usa codigo de reserva para los coles, vale con filtrar por aquellos que tengan codigo de reserva + case 'PARTNERS_ALL': + userIds = await eventInscriptionService._getInscriptionByEventFromPartner(eventId); + break; + + //Todos los inscritos al evento, tanto validados como en lista de espera + default: //ALL + userIds = await eventInscriptionService._getInscriptionByEvent(eventId); + break; + } + + return new Promise(function (resolve) { resolve(usersIdsComposer(userIds)); }); + + } else { + return handleErrorResponse(controllerOptions.MODULE_NAME, 'sendNotification', new Error('Missing event and segment'), res) + } + } + + receipt = notificationService.sendNotification(notification, await getUserIds()); - return Promise.all([_saveStatus, _disableDevices]); - }) - .then(function (details) { - console.log(details); - }); } catch (error) { console.error(error); return handleErrorResponse(controllerOptions.MODULE_NAME, 'sendNotification', error, res) diff --git a/modules/notification/notification.model.js b/modules/notification/notification.model.js index 469ec2f..76fdbd5 100644 --- a/modules/notification/notification.model.js +++ b/modules/notification/notification.model.js @@ -26,6 +26,10 @@ module.exports = function (sequelize, DataTypes) { allowNull: false, default: 'default', }, + recipients: { + type: DataTypes.JSON, + allowNull: true, + }, data: { type: DataTypes.JSON, allowNull: true, diff --git a/modules/notification/notification.routes.js b/modules/notification/notification.routes.js index 41eb88a..c22f692 100644 --- a/modules/notification/notification.routes.js +++ b/modules/notification/notification.routes.js @@ -6,7 +6,7 @@ const PaginateMiddleware = require('../../middlewares/paginate'); const FieldMiddleware = require('../../middlewares/fields'); const SortMiddleware = require('../../middlewares/sort'); const notificationController = require('./notification.controller'); -const { pushSendEvent, deviceTokenInputType, notificationSendType } = require('./notification.validations'); +const { pushSendEvent, deviceTokenInputType } = require('./notification.validations'); const generalInvalidFields = [ 'createdAt', 'updatedAt', @@ -36,17 +36,17 @@ routes.get('/admin/notifications/:id', routes.post('/admin/notifications', isAdministratorUser, - SchemaValidator(notificationSendType, true), + SchemaValidator(pushSendEvent, true), notificationController.sendNotification({ scopes: ['defaultScope'] }) ); -routes.post('/admin/notifications/event', +/*routes.post('/admin/notifications/event', isAdministratorUser, SchemaValidator(pushSendEvent, true), notificationController.sendNotificationEvent -); +);*/ /* Borrar cuando ya no aparezca la versión 1.0.10 */ routes.post('/notifications/register', diff --git a/modules/notification/notification.service.js b/modules/notification/notification.service.js index ef93031..bdd6106 100644 --- a/modules/notification/notification.service.js +++ b/modules/notification/notification.service.js @@ -1,74 +1,137 @@ -const { Expo } = require('expo-server-sdk'); -const { generateService, parseParamsToFindOptions } = require('../../helpers/service.helper'); -const models = require('../../core/models'); +const moment = require('moment'); -// Create a new Expo SDK client -const expo = new Expo(); +const { generateService, parseParamsToFindOptions } = require('../../helpers/service.helper'); +const userDeviceService = require('./user_device.service'); +const notificationDetailService = require('./notification_detail.service'); +const pushHelper = require('../../helpers/push.helper'); +const models = require('../../core/models'); const extraMethods = { - createNotification: (date, title, body, ttl, priority, data, userId) => { - console.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>< { + return { + date: data.date, + title: data.title, + body: data.body, + ttl: data.ttl, + priority: data.priority, + recipients: data.recipients, + data: data.data, + userId: data.userId, + } + }, - return new Promise(function (resolve, reject) { - models.Notification.create({ - date: date, - title: title, - body: body, - userId: userId, - ttl: ttl, - priority: priority, - data: data, - }) - .then(function (result) { - resolve(result); - }) - .catch(function (error) { - reject(error) - }); + saveNotification: ({ date, title, body, ttl, priority, recipients, data, userId }) => { + return models.Notification.create({ + date: moment(date), + title: title, + body: body, + userId: userId, + ttl: ttl ? ttl : undefined, + priority: priority ? priority : 'default', + recipients: recipients, + data: data, }); }, + - - - sendNotification: async (messages) => { + sendNotification: (notification, userIds) => { - // The Expo push notification service accepts batches of notifications so - // that you don't need to send 1000 requests to send 1000 notifications. We - // recommend you batch your notifications to reduce the number of requests - // and to compress them (notifications with similar content will get - // compressed). + console.log('sendNofitication -----------------------------------------------'); + console.log(notification, userIds); - /** - * There is a limit on the number of push notifications (100) you can send at once.Use - * `chunkPushNotifications` to divide an array of push notification messages into appropriately - * sized chunks - */ + let getUserDevicesPromise = (userId) => userDeviceService.fetchAll({ params: { userId: userId } }, {}).then(function (result) { + return new Promise(function (resolve) { resolve(result.rows) }); + }); + let saveNotificationPromise = (notification) => extraMethods.saveNotification(notification); + let sendNotificationsPromise = (messages) => pushHelper.sendPushMessage(messages); + let disableUserDevicePromise = (token) => userDeviceService.update({ + params: { + token: token, + } + }, { valid: 0, invalidated: moment() }, {}); - // Later, after the Expo push notification service has delivered the - // notifications to Apple or Google (usually quickly, but allow the the service - // up to 30 minutes when under load), a "receipt" for each notification is - // created. The receipts will be available for at least a day; stale receipts - // are deleted. - // - // The ID of each receipt is sent back in the response "ticket" for each - // notification. In summary, sending a notification produces a ticket, which - // contains a receipt ID you later use to get the receipt. - // - // The receipts may contain error codes to which you must respond. In - // particular, Apple or Google may block apps that continue to send - // notifications to devices that have blocked notifications or have uninstalled - // your app. Expo does not control this policy and sends back the feedback from - // Apple and Google so you can handle it appropriately. + let disableInvalidUserDevicesPromise = (userDevices) => { + return new Promise(function (resolve) { + let _userDevices = []; + userDevices.forEach(async function (userDevice) { + if (!userDeviceService.isValidPushToken(userDevice.token)) { + await disableUserDevicePromise(userDevice.token); + } else { + _userDevices.push(userDevice); + } + }); - let chunks = expo.chunkPushNotifications(messages); - return { - messages, - tickets: await _sendPushNotificationsAsync(chunks) + resolve(_userDevices) + }); }; + + let disableUserDevicesWithErrorStatus = (messages, tickets) => { + return new Promise(function (resolve) { + tickets.forEach(async function (ticket, index) { + if ((ticket.status === 'error') && (ticket.details.error === 'DeviceNotRegistered')) { + await disableUserDevicePromise(messages[index].to) + } + }); + resolve(true); + }); + } + + let saveResponseStatusPromise = (messages, tickets) => notificationDetailService.saveNotificationDetails(messages, tickets); + + + let buildMessagesPromise = (userDevices) => { + return new Promise(function (resolve) { + let messages = []; + + userDevices.forEach(async function (userDevice) { + messages.push(pushHelper.createPushMessage({ + ...notification, + userId: userDevice.userId, + to: userDevice.token, + })); + }); + resolve(messages) + }); + }; + + + let getUserDevicesList = []; + + saveNotificationPromise(notification) + .then(function (notificationRecord) { + notification = notificationRecord.toJSON(); + userIds.forEach(function (userId) { + getUserDevicesList.push(getUserDevicesPromise(userId)); + }); + + return Promise.all(getUserDevicesList) + }).then(function (userDeviceList) { + let result = []; + userDeviceList.forEach(function (elements) { + elements.forEach(function (item) { + result.push(item) + }) + }); + return new Promise(function (resolve) { resolve(result); }); + }) + .then(disableInvalidUserDevicesPromise) + .then(buildMessagesPromise) + .then(sendNotificationsPromise) + .then(function ({ messages, tickets }) { + let _saveStatus = saveResponseStatusPromise(messages, tickets); + let _disableDevices = disableUserDevicesWithErrorStatus(messages, tickets); + + return Promise.all([_saveStatus, _disableDevices]); + }) + .then(function (details) { + console.log(details); + return details; + }); }, + + getNotificationsWithoutReceipt: async() => { }, @@ -84,67 +147,4 @@ const extraMethods = { module.exports = generateService(models.Notification, extraMethods); -const _sendPushNotificationsAsync = async function (chunks) { - return new Promise(async function (resolve) { - let tickets = []; - // Send the chunks to the Expo push notification service. There are - // different strategies you could use. A simple one is to send one chunk at a - // time, which nicely spreads the load out over time: - for (let chunk of chunks) { - try { - let ticketChunk = await expo.sendPushNotificationsAsync(chunk); - tickets.push(...ticketChunk); - - // NOTE: If a ticket contains an error code in ticket.details.error, you - // must handle it appropriately. The error codes are listed in the Expo - // documentation: - // https://docs.expo.io/versions/latest/guides/push-notifications#response-format - - } catch (error) { - console.error(error); - } - } - - resolve(tickets); - }); -}; - -const _getPushNotificationsResultAsync = async function (receiptIdChunks) { - return new Promise(async function (resolve) { - // Like sending notifications, there are different strategies you could use - // to retrieve batches of receipts from the Expo service. - - let result = []; - - console.log(receiptIdChunks); - - for (let chunk of receiptIdChunks) { - try { - let receipts = await expo.getPushNotificationReceiptsAsync(chunk); - console.log('hola', receipts); - - // The receipts specify whether Apple or Google successfully received the - // notification and information about an error, if one occurred. - for (let key in receipts) { - if (receipts[key].status === 'ok') { - result.push[receipts[key]]; - continue; - } else if (receipts[key].status === 'error') { - console.error(`There was an error sending a notification: ${receipts[key].message}`); - if (receipts[key].details && receipts[key].details.error) { - // The error codes are listed in the Expo documentation: - // https://docs.expo.io/versions/latest/guides/push-notifications#response-format - // You must handle the errors appropriately. - console.error(`The error code is ${receipts[key].details.error}`); - } - } - } - } catch (error) { - console.error(error); - } - } - - resolve(result); - }); -} diff --git a/modules/notification/notification.validations.js b/modules/notification/notification.validations.js index 43a38a8..5f051b1 100644 --- a/modules/notification/notification.validations.js +++ b/modules/notification/notification.validations.js @@ -4,21 +4,24 @@ const deviceTokenInputType = Joi.object().keys({ token: Joi.string().required(), }); -const pushSendType = Joi.object().keys({ +/*const pushSendType = Joi.object().keys({ userIds: Joi.array().required(), title: Joi.string().required(), message: Joi.string().required(), //token: Joi.string().required(), -}); +});*/ const pushSendEvent = Joi.object().keys({ - date: Joi.date().required(), + date: Joi.date().optional(), title: Joi.string().required(), - message: Joi.string().required(), + body: Joi.string().required(), recipients: Joi.object().keys({ - eventId: Joi.string().required(), - segment: Joi.string().required(), + userIds: Joi.array().optional(), + eventId: Joi.string().optional(), + segment: Joi.string().optional(), }), + priority: Joi.string().optional(), + ttl: Joi.string().optional(), data: Joi.object().keys({ type: Joi.string().required(), title: Joi.string().required(), @@ -33,5 +36,5 @@ const pushSendEvent = Joi.object().keys({ }); module.exports = { - deviceTokenInputType, pushSendType, pushSendEvent + deviceTokenInputType, pushSendEvent }; diff --git a/package.json b/package.json index 07a4980..20b61fe 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "sanitize-filename": "^1.6.2", "sequelize": "^5.16.0", "sharp": "^0.23.0", + "tinytim": "^0.1.1", "unique-file-name": "^1.0.0", "vimeo": "^2.1.1", "vm": "^0.1.0",