Gestión de notificaciones push
This commit is contained in:
parent
73f90217d4
commit
dbd615086a
42
modules/push/notification.model.js
Normal file
42
modules/push/notification.model.js
Normal file
@ -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;
|
||||||
|
};
|
||||||
|
|
||||||
67
modules/push/notification_detail.model.js
Normal file
67
modules/push/notification_detail.model.js
Normal file
@ -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;
|
||||||
|
};
|
||||||
|
|
||||||
@ -15,31 +15,60 @@ const extraControllers = {
|
|||||||
sendNotification: (config) => {
|
sendNotification: (config) => {
|
||||||
return async (req, res, next) => {
|
return async (req, res, next) => {
|
||||||
let receipt = undefined;
|
let receipt = undefined;
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
try {
|
||||||
const context = buildContext(req, config);
|
let getUserDevicesPromise = (userId) => pushService.fetchAll({ params: { userId: userId }}, context);
|
||||||
let params = extractParamsFromRequest(req, res);
|
let sendNotificationsPromise = (messages) => pushService.sendNotification(messages);
|
||||||
console.log(context, params);
|
|
||||||
|
|
||||||
let token = undefined;
|
let buildMessagePromise = (userDevices) => {
|
||||||
if (params.userId) {
|
let message = undefined;
|
||||||
token = await pushService._getPushToken({
|
userDevices.rows.forEach(function (userDevice) {
|
||||||
userId: params.userId});
|
if (pushService.isValidPushToken(userDevice.token)) {
|
||||||
}
|
message = {
|
||||||
|
userId: userDevice.userId,
|
||||||
const message = {
|
to: userDevice.token,
|
||||||
to: token,
|
title: req.body.title,
|
||||||
sound: 'default',
|
body: req.body.message,
|
||||||
title: req.body.title,
|
ttl: req.body.ttl,
|
||||||
body: req.body.message,
|
priority: req.body.priority,
|
||||||
|
data: req.body.data,
|
||||||
|
sound: 'default',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new Promise(function(resolve) { resolve(message) });
|
||||||
};
|
};
|
||||||
|
|
||||||
receipt = await pushService.sendNotification(message);
|
let getUserDevicesList = [];
|
||||||
|
|
||||||
|
userIds.forEach(function (userId) {
|
||||||
|
getUserDevicesList.push(getUserDevicesPromise(userId));
|
||||||
|
});
|
||||||
|
|
||||||
|
receipt = await Promise.all(getUserDevicesList).then(
|
||||||
|
function(userDeviceList) {
|
||||||
|
return Promise.all(userDeviceList.map(buildMessagePromise))
|
||||||
|
})
|
||||||
|
.then(sendNotificationsPromise)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
return handleErrorResponse(controllerOptions.MODULE_NAME, 'sendNotification', error, res)
|
||||||
} finally {
|
} finally {
|
||||||
// En todo caso devolver OK al cliente
|
|
||||||
return handleResultResponse(receipt, null, null, res, httpStatus.OK);
|
return handleResultResponse(receipt, null, null, res, httpStatus.OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,7 +103,7 @@ const extraControllers = {
|
|||||||
} else {
|
} else {
|
||||||
// Actualizar el token
|
// Actualizar el token
|
||||||
console.log('>> Actualizar el token', params.params, data, context);
|
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) {
|
} catch(error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@ -11,16 +11,42 @@ const generalInvalidFields = [
|
|||||||
'createdAt', 'updatedAt',
|
'createdAt', 'updatedAt',
|
||||||
];
|
];
|
||||||
|
|
||||||
routes.post('/notifications/send/',
|
routes.get('/notifications',
|
||||||
isAdministratorUser,
|
/*isAdministratorUser,
|
||||||
SchemaValidator(pushSendType, true),
|
SchemaValidator(pushSendType, true),
|
||||||
pushTokenController.sendNotification()
|
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',
|
routes.post('/notifications/register',
|
||||||
isLoggedUser,
|
isLoggedUser,
|
||||||
SchemaValidator(pushInputType, true),
|
SchemaValidator(pushInputType, true),
|
||||||
pushTokenController.registerUser()
|
pushTokenController.registerUser()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
routes.post('/notifications/devices',
|
||||||
|
isLoggedUser,
|
||||||
|
SchemaValidator(pushInputType, true),
|
||||||
|
pushTokenController.registerUser()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
module.exports = routes;
|
module.exports = routes;
|
||||||
@ -1,13 +1,13 @@
|
|||||||
const _ = require('lodash');
|
const moment = require('moment');
|
||||||
const Expo = require('expo-server-sdk');
|
const { Expo } = require('expo-server-sdk');
|
||||||
const { generateService, parseParamsToFindOptions } = require('../../helpers/service.helper');
|
const { generateService, parseParamsToFindOptions } = require('../../helpers/service.helper');
|
||||||
const models = require('../../core/models');
|
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 = {
|
const extraMethods = {
|
||||||
|
|
||||||
|
|
||||||
isValidPushToken: (token) => {
|
isValidPushToken: (token) => {
|
||||||
return Expo.isExpoPushToken(token);
|
return Expo.isExpoPushToken(token);
|
||||||
},
|
},
|
||||||
@ -18,36 +18,19 @@ const extraMethods = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
sendNotification: (message) => {
|
sendNotification: async (messages) => {
|
||||||
if (!extraMethods.isValidPushToken(message.to)) {
|
|
||||||
throw new Error(`Push token ${message.to} is not a valid Expo push token`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Expo push notification service accepts batches of notifications so
|
// The Expo push notification service accepts batches of notifications so
|
||||||
// that you don't need to send 1000 requests to send 1000 notifications. We
|
// 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
|
// recommend you batch your notifications to reduce the number of requests
|
||||||
// and to compress them (notifications with similar content will get
|
// and to compress them (notifications with similar content will get
|
||||||
// compressed).
|
// compressed).
|
||||||
let chunks = expo.chunkPushNotifications(message);
|
|
||||||
let tickets = [];
|
/**
|
||||||
(async () => {
|
* There is a limit on the number of push notifications (100) you can send at once.Use
|
||||||
// Send the chunks to the Expo push notification service. There are
|
* `chunkPushNotifications` to divide an array of push notification messages into appropriately
|
||||||
// different strategies you could use. A simple one is to send one chunk at a
|
* sized chunks
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Later, after the Expo push notification service has delivered the
|
// Later, after the Expo push notification service has delivered the
|
||||||
// notifications to Apple or Google (usually quickly, but allow the the service
|
// 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
|
// notifications to devices that have blocked notifications or have uninstalled
|
||||||
// your app. Expo does not control this policy and sends back the feedback from
|
// your app. Expo does not control this policy and sends back the feedback from
|
||||||
// Apple and Google so you can handle it appropriately.
|
// Apple and Google so you can handle it appropriately.
|
||||||
|
|
||||||
|
let chunks = expo.chunkPushNotifications(messages);
|
||||||
|
let tickets = await _sendPushNotificationsAsync(chunks);
|
||||||
|
|
||||||
|
console.log(tickets);
|
||||||
let receiptIds = [];
|
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
|
// NOTE: Not all tickets have IDs; for example, tickets for notifications
|
||||||
// that could not be enqueued will have error information and no receipt ID.
|
// that could not be enqueued will have error information and no receipt ID.
|
||||||
if (ticket.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);
|
console.log(receiptIds);
|
||||||
(async () => {
|
console.log(invalidTokens);
|
||||||
// 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);
|
|
||||||
|
|
||||||
// The receipts specify whether Apple or Google successfully received the
|
|
||||||
// notification and information about an error, if one occurred.
|
let receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds);
|
||||||
for (let receipt of receipts) {
|
let xxx = await _getPushNotificationsResultAsync(receiptIdChunks);
|
||||||
if (receipt.status === 'ok') {
|
|
||||||
continue;
|
let notifications = await _saveNotifications(messages, tickets);
|
||||||
} else if (receipt.status === 'error') {
|
|
||||||
console.error(`There was an error sending a notification: ${receipt.message}`);
|
return new Promise(function (resolve) { resolve(notifications) });
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = generateService(models.UserDevice, extraMethods);
|
module.exports = generateService(models.UserDevice, extraMethods);
|
||||||
|
|
||||||
|
|
||||||
|
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) });
|
||||||
|
}
|
||||||
@ -5,6 +5,9 @@ const pushInputType = Joi.object().keys({
|
|||||||
});
|
});
|
||||||
|
|
||||||
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(),
|
//token: Joi.string().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,12 @@ module.exports = function (sequelize, DataTypes) {
|
|||||||
tableName: 'users_devices',
|
tableName: 'users_devices',
|
||||||
freezeTableName: true,
|
freezeTableName: true,
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
|
||||||
|
defaultScope: {
|
||||||
|
where: {
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
UserDevice.associate = function (models) {
|
UserDevice.associate = function (models) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user