diff --git a/modules/push/notification.model.js b/modules/push/notification.model.js new file mode 100644 index 0000000..1bf2803 --- /dev/null +++ b/modules/push/notification.model.js @@ -0,0 +1,42 @@ +module.exports = function (sequelize, DataTypes) { + const Notification = sequelize.define('Notification', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + title: { + type: DataTypes.STRING, + allowNull: true, + }, + body: { + type: DataTypes.STRING, + allowNull: true, + }, + ttl: { + type: DataTypes.INTEGER, + allowNull: true, + }, + priority: { + type: DataTypes.STRING, + allowNull: false, + default: 'default', + }, + data: { + type: DataTypes.JSON, + allowNull: true, + }, + }, { + tableName: 'notifications', + freezeTableName: true, + timestamps: true, + }); + + Notification.associate = function (models) { + Notification.User = Notification.belongsTo(models.User, { foreignKey: 'userId', as: "user" }); + Notification.Details = Notification.hasMany(models.NotificationDetail, { foreignKey: 'notificationId', as: 'details' }); + }; + + return Notification; +}; + \ No newline at end of file diff --git a/modules/push/notification_detail.model.js b/modules/push/notification_detail.model.js new file mode 100644 index 0000000..805db8d --- /dev/null +++ b/modules/push/notification_detail.model.js @@ -0,0 +1,67 @@ +module.exports = function (sequelize, DataTypes) { + const NotificationDetail = sequelize.define('NotificationDetail', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + token: { + type: DataTypes.STRING, + allowNull: false, + }, + title: { + type: DataTypes.STRING, + allowNull: true, + }, + body: { + type: DataTypes.STRING, + allowNull: true, + }, + ttl: { + type: DataTypes.INTEGER, + allowNull: true, + }, + priority: { + type: DataTypes.STRING, + allowNull: false, + default: 'default', + }, + data: { + type: DataTypes.JSON, + allowNull: true, + }, + ticket: { + type: DataTypes.STRING, + allowNull: false, + }, + deliveryDate: { + type: DataTypes.DATE, + allowNull: false, + default: DataTypes.NOW, + }, + deliveryStatus: { + type: DataTypes.STRING, + allowNull: true, + }, + receiptDate: { + type: DataTypes.DATE, + allowNull: true, + }, + receiptStatus: { + type: DataTypes.STRING, + allowNull: true, + }, + }, { + tableName: 'notifications_details', + freezeTableName: true, + timestamps: true, + }); + + NotificationDetail.associate = function (models) { + NotificationDetail.User = NotificationDetail.belongsTo(models.User, { foreignKey: 'userId', as: "user" }); + //NotificationDetail.Notification = NotificationDetail.belongsTo(models.Notification, { foreignKey: 'notificationId', as: "user" }); + }; + + return NotificationDetail; +}; + \ No newline at end of file diff --git a/modules/push/push.controller.js b/modules/push/push.controller.js index 18f5bd2..8c691d0 100644 --- a/modules/push/push.controller.js +++ b/modules/push/push.controller.js @@ -15,31 +15,60 @@ const extraControllers = { sendNotification: (config) => { return async (req, res, next) => { let receipt = undefined; - try { - const context = buildContext(req, config); - let params = extractParamsFromRequest(req, res); - console.log(context, params); + const context = buildContext(req, config); + let params = extractParamsFromRequest(req, res); - let token = undefined; - if (params.userId) { - token = await pushService._getPushToken({ - userId: params.userId}); - } + let userIds = req.body.userIds; + if (!userIds) { + return handleErrorResponse(controllerOptions.MODULE_NAME, 'sendNotification', new Error('Missing user Ids'), res) + } - const message = { - to: token, - sound: 'default', - title: req.body.title, - body: req.body.message, + if (!req.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) + } + + try { + let getUserDevicesPromise = (userId) => pushService.fetchAll({ params: { userId: userId }}, context); + let sendNotificationsPromise = (messages) => pushService.sendNotification(messages); + + let buildMessagePromise = (userDevices) => { + let message = undefined; + userDevices.rows.forEach(function (userDevice) { + if (pushService.isValidPushToken(userDevice.token)) { + message = { + userId: userDevice.userId, + to: userDevice.token, + title: req.body.title, + body: req.body.message, + ttl: req.body.ttl, + priority: req.body.priority, + data: req.body.data, + sound: 'default', + }; + } + }); + return new Promise(function(resolve) { resolve(message) }); }; + + let getUserDevicesList = []; + + userIds.forEach(function (userId) { + getUserDevicesList.push(getUserDevicesPromise(userId)); + }); - receipt = await pushService.sendNotification(message); - + receipt = await Promise.all(getUserDevicesList).then( + function(userDeviceList) { + return Promise.all(userDeviceList.map(buildMessagePromise)) + }) + .then(sendNotificationsPromise) } catch (error) { - console.error(error); + return handleErrorResponse(controllerOptions.MODULE_NAME, 'sendNotification', error, res) } finally { - // En todo caso devolver OK al cliente return handleResultResponse(receipt, null, null, res, httpStatus.OK); } } @@ -74,7 +103,7 @@ const extraControllers = { } else { // Actualizar el token console.log('>> Actualizar el token', params.params, data, context); - result = await pushService.update(params, data, context); + result = await pushService.update(params.params, data, context); } } catch(error) { console.error(error); diff --git a/modules/push/push.routes.js b/modules/push/push.routes.js index 0ac4d91..0ca10c1 100644 --- a/modules/push/push.routes.js +++ b/modules/push/push.routes.js @@ -11,16 +11,42 @@ const generalInvalidFields = [ 'createdAt', 'updatedAt', ]; -routes.post('/notifications/send/', - isAdministratorUser, - SchemaValidator(pushSendType, true), - pushTokenController.sendNotification() +routes.get('/notifications', + /*isAdministratorUser, + SchemaValidator(pushSendType, true), + pushTokenController.sendNotification({ + scopes: ['defaultScope'] + })*/ ); +routes.get('/notifications/:id', + /*isAdministratorUser, + SchemaValidator(pushSendType, true), + pushTokenController.sendNotification({ + scopes: ['defaultScope'] + })*/ +); + +routes.post('/notifications', + isAdministratorUser, + SchemaValidator(pushSendType, true), + pushTokenController.sendNotification({ + scopes: ['defaultScope'] + }) +); + +/* Borrar cuando ya no aparezca la versión 1.0.10 */ routes.post('/notifications/register', isLoggedUser, SchemaValidator(pushInputType, true), pushTokenController.registerUser() ); +routes.post('/notifications/devices', + isLoggedUser, + SchemaValidator(pushInputType, true), + pushTokenController.registerUser() +); + + module.exports = routes; \ No newline at end of file diff --git a/modules/push/push.service.js b/modules/push/push.service.js index e13abff..3a97f1e 100644 --- a/modules/push/push.service.js +++ b/modules/push/push.service.js @@ -1,13 +1,13 @@ -const _ = require('lodash'); -const Expo = require('expo-server-sdk'); +const moment = require('moment'); +const { Expo } = require('expo-server-sdk'); const { generateService, parseParamsToFindOptions } = require('../../helpers/service.helper'); const models = require('../../core/models'); -const cdnHelper = require('../../helpers/cdn.helper'); -const { extractProviderInfo } = require('../../helpers/providers.helper'); + +// Create a new Expo SDK client +const expo = new Expo(); const extraMethods = { - isValidPushToken: (token) => { return Expo.isExpoPushToken(token); }, @@ -18,36 +18,19 @@ const extraMethods = { }); }, - sendNotification: (message) => { - if (!extraMethods.isValidPushToken(message.to)) { - throw new Error(`Push token ${message.to} is not a valid Expo push token`); - } + sendNotification: 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). - let chunks = expo.chunkPushNotifications(message); - let tickets = []; - (async () => { - // 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); - console.log(ticketChunk); - 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); - } - } - })(); + + /** + * 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 @@ -64,46 +47,116 @@ const extraMethods = { // 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); + let tickets = await _sendPushNotificationsAsync(chunks); + + console.log(tickets); let receiptIds = []; - for (let ticket of tickets) { + let invalidTokens = []; + for (let [key, ticket] of tickets.entries()) { // NOTE: Not all tickets have IDs; for example, tickets for notifications // that could not be enqueued will have error information and no receipt ID. if (ticket.id) { - receiptIds.push(ticket.id); + receiptIds.push(ticket); + } else { + if ((ticket.status === 'error') && (ticket.details.error === 'DeviceNotRegistered')) { + invalidTokens.push({ + ...messages[key], + valid: false, + invalidated: moment(), + }); + } } } - let receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds); - (async () => { - // Like sending notifications, there are different strategies you could use - // to retrieve batches of receipts from the Expo service. - for (let chunk of receiptIdChunks) { - try { - let receipts = await expo.getPushNotificationReceiptsAsync(chunk); - console.log(receipts); + console.log(receiptIds); + console.log(invalidTokens); - // The receipts specify whether Apple or Google successfully received the - // notification and information about an error, if one occurred. - for (let receipt of receipts) { - if (receipt.status === 'ok') { - continue; - } else if (receipt.status === 'error') { - console.error(`There was an error sending a notification: ${receipt.message}`); - if (receipt.details && receipt.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 ${receipt.details.error}`); - } - } - } - } catch (error) { - console.error(error); - } - } - })(); + + let receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds); + let xxx = await _getPushNotificationsResultAsync(receiptIdChunks); + + let notifications = await _saveNotifications(messages, tickets); + + return new Promise(function (resolve) { resolve(notifications) }); } }; +module.exports = generateService(models.UserDevice, extraMethods); -module.exports = generateService(models.UserDevice, extraMethods); \ No newline at end of file + +const _sendPushNotificationsAsync = async function (chunks) { + 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); + } + } + + return new Promise(function (resolve) { resolve(tickets) }); +}; + +const _getPushNotificationsResultAsync = async function (receiptIdChunks) { + // 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); + } + } + + return new Promise(function (resolve) { resolve(result) }); +} + +const _saveNotifications = async function (messages, tickets) { + let notifications = []; + messages.forEach(function (message, index) { + let notification = models.Notification.build({ + ...message, + ticket: tickets[index].id, + status: tickets[index].status, + error: (tickets[index].status === 'error') ? tickets[index].details.error : undefined, + }); + notifications.push(notification); + }); + + return new Promise(function (resolve) { resolve(notifications) }); +} \ No newline at end of file diff --git a/modules/push/push.validations.js b/modules/push/push.validations.js index bfae5bc..d51f9fa 100644 --- a/modules/push/push.validations.js +++ b/modules/push/push.validations.js @@ -5,6 +5,9 @@ const pushInputType = Joi.object().keys({ }); const pushSendType = Joi.object().keys({ + userIds: Joi.array().required(), + title: Joi.string().required(), + message: Joi.string().required(), //token: Joi.string().required(), }); diff --git a/modules/push/user_device.model.js b/modules/push/user_device.model.js index 32dc120..89a82f2 100644 --- a/modules/push/user_device.model.js +++ b/modules/push/user_device.model.js @@ -20,6 +20,12 @@ module.exports = function (sequelize, DataTypes) { tableName: 'users_devices', freezeTableName: true, timestamps: true, + + defaultScope: { + where: { + valid: true, + }, + }, }); UserDevice.associate = function (models) {