Files
kami-parse-server/src/GCM.js
Florent Vilmart dad50d12f5 Upgrade APNS to use HTTP/2
- uses universal certificate
- removes tests logs
- standardized returned promises from APNS and GCM to something usable in _PushStatus
2016-03-13 18:15:15 -04:00

152 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
const Parse = require('parse/node').Parse;
const gcm = require('node-gcm');
const cryptoUtils = require('./cryptoUtils');
const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
const GCMRegistrationTokensMax = 1000;
function GCM(args) {
if (typeof args !== 'object' || !args.apiKey) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'GCM Configuration is invalid');
}
this.sender = new gcm.Sender(args.apiKey);
}
/**
* Send gcm request.
* @param {Object} data The data we need to send, the format is the same with api request body
* @param {Array} devices A array of devices
* @returns {Object} A promise which is resolved after we get results from gcm
*/
GCM.prototype.send = function(data, devices) {
// Make a new array
devices = new Array(...devices);
let timestamp = Date.now();
// For android, we can only have 1000 recepients per send, so we need to slice devices to
// chunk if necessary
let slices = sliceDevices(devices, GCMRegistrationTokensMax);
if (slices.length > 1) {
// Make 1 send per slice
let promises = slices.reduce((memo, slice) => {
let promise = this.send(data, slice, timestamp);
memo.push(promise);
return memo;
}, [])
return Parse.Promise.when(promises).then((results) => {
let allResults = results.reduce((memo, result) => {
return memo.concat(result);
}, []);
return Parse.Promise.as(allResults);
});
}
// get the devices back...
devices = slices[0];
let expirationTime;
// We handle the expiration_time convertion in push.js, so expiration_time is a valid date
// in Unix epoch time in milliseconds here
if (data['expiration_time']) {
expirationTime = data['expiration_time'];
}
// Generate gcm payload
let gcmPayload = generateGCMPayload(data.data, timestamp, expirationTime);
// Make and send gcm request
let message = new gcm.Message(gcmPayload);
// Build a device map
let devicesMap = devices.reduce((memo, device) => {
memo[device.deviceToken] = device;
return memo;
}, {});
let deviceTokens = Object.keys(devicesMap);
let promises = deviceTokens.map(() => new Parse.Promise());
let registrationTokens = deviceTokens;
this.sender.send(message, { registrationTokens: registrationTokens }, 5, (error, response) => {
// example response:
/*
{ "multicast_id":7680139367771848000,
"success":0,
"failure":4,
"canonical_ids":0,
"results":[ {"error":"InvalidRegistration"},
{"error":"InvalidRegistration"},
{"error":"InvalidRegistration"},
{"error":"InvalidRegistration"}] }
*/
let { results, multicast_id } = response || {};
registrationTokens.forEach((token, index) => {
let promise = promises[index];
let result = results ? results[index] : undefined;
let device = devicesMap[token];
let resolution = {
device,
multicast_id,
response: error || result,
};
if (!result || result.error) {
resolution.transmitted = false;
} else {
resolution.transmitted = true;
}
promise.resolve(resolution);
});
});
return Parse.Promise.when(promises);
}
/**
* Generate the gcm payload from the data we get from api request.
* @param {Object} coreData The data field under api request body
* @param {String} pushId A random string
* @param {Number} timeStamp A number whose format is the Unix Epoch
* @param {Number|undefined} expirationTime A number whose format is the Unix Epoch or undefined
* @returns {Object} A promise which is resolved after we get results from gcm
*/
function generateGCMPayload(coreData, timeStamp, expirationTime) {
let payloadData = {
'time': new Date(timeStamp).toISOString(),
'data': JSON.stringify(coreData)
}
let payload = {
priority: 'normal',
data: payloadData
};
if (expirationTime) {
// The timeStamp and expiration is in milliseconds but gcm requires second
let timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
if (timeToLive < 0) {
timeToLive = 0;
}
if (timeToLive >= GCMTimeToLiveMax) {
timeToLive = GCMTimeToLiveMax;
}
payload.timeToLive = timeToLive;
}
return payload;
}
/**
* Slice a list of devices to several list of devices with fixed chunk size.
* @param {Array} devices An array of devices
* @param {Number} chunkSize The size of the a chunk
* @returns {Array} An array which contaisn several arries of devices with fixed chunk size
*/
function sliceDevices(devices, chunkSize) {
let chunkDevices = [];
while (devices.length > 0) {
chunkDevices.push(devices.splice(0, chunkSize));
}
return chunkDevices;
}
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
GCM.generateGCMPayload = generateGCMPayload;
GCM.sliceDevices = sliceDevices;
}
module.exports = GCM;