Skip to content

Commit

Permalink
Add support for push
Browse files Browse the repository at this point in the history
  • Loading branch information
wangmengyan95 committed Feb 8, 2016
1 parent 123ac5f commit 681809f
Show file tree
Hide file tree
Showing 8 changed files with 448 additions and 21 deletions.
4 changes: 4 additions & 0 deletions APNS.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ function APNS(args) {
});

this.sender.on("socketError", console.error);

this.sender.on("transmitted", function(notification, device) {
console.log("Notification transmitted to:" + device.token.toString("hex"));
});
}

/**
Expand Down
10 changes: 8 additions & 2 deletions GCM.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ var randomstring = require('randomstring');
var GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
var GCMRegistrationTokensMax = 1000;

function GCM(apiKey) {
this.sender = new gcm.Sender(apiKey);
function GCM(args) {
this.sender = new gcm.Sender(args.apiKey);
}

/**
Expand Down Expand Up @@ -39,6 +39,10 @@ GCM.prototype.send = function (data, registrationTokens) {
this.sender.send(message, { registrationTokens: registrationTokens }, 5, function (error, response) {
// TODO: Use the response from gcm to generate and save push report
// TODO: If gcm returns some deviceTokens are invalid, set tombstone for the installation
console.log('GCM request and response %j', {
request: message,
response: response
});
promise.resolve();
});
return promise;
Expand Down Expand Up @@ -76,6 +80,8 @@ var generateGCMPayload = function(coreData, pushId, timeStamp, expirationTime) {
return payload;
}

GCM.GCMRegistrationTokensMax = GCMRegistrationTokensMax;

if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
GCM.generateGCMPayload = generateGCMPayload;
}
Expand Down
149 changes: 149 additions & 0 deletions ParsePushAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// ParsePushAdapter is the default implementation of
// PushAdapter, it uses GCM for android push and APNS
// for ios push.
var Parse = require('parse/node').Parse,
GCM = require('./GCM'),
APNS = require('./APNS');

function ParsePushAdapter() {
this.validPushTypes = ['ios', 'android'];
this.senders = {};
}

ParsePushAdapter.prototype.registerPushSenders = function(pushConfig) {
// Initialize senders
for (var i = 0; i < this.validPushTypes.length; i++) {
this.senders[this.validPushTypes[i]] = [];
}

pushConfig = pushConfig || {};
var pushTypes = Object.keys(pushConfig);
for (var i = 0; i < pushTypes.length; i++) {
var pushType = pushTypes[i];
if (this.validPushTypes.indexOf(pushType) < 0) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push to ' + pushTypes + ' is not supported');
}

var typePushConfig = pushConfig[pushType];
var senderArgs = [];
// Since for ios, there maybe multiple cert/key pairs,
// typePushConfig can be an array.
if (Array.isArray(typePushConfig)) {
senderArgs = senderArgs.concat(typePushConfig);
} else if (typeof typePushConfig === 'object') {
senderArgs.push(typePushConfig);
} else {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push Configuration is invalid');
}
for (var j = 0; j < senderArgs.length; j++) {
var senderArg = senderArgs[j];
var sender;
switch (pushType) {
case 'ios':
sender = new APNS(senderArg);
break;
case 'android':
sender = new GCM(senderArg);
break;
}
this.senders[pushType].push(sender);
}
}
}

/**
* Get an array of push senders based on the push type.
* @param {String} The push type
* @returns {Array|Undefined} An array of push senders
*/
ParsePushAdapter.prototype.getPushSenders = function(pushType) {
if (!this.senders[pushType]) {
console.log('No push sender for push type %s', pushType);
return [];
}
return this.senders[pushType];
}

/**
* Get an array of valid push types.
* @returns {Array} An array of valid push types
*/
ParsePushAdapter.prototype.getValidPushTypes = function() {
return this.validPushTypes;
}

ParsePushAdapter.prototype.send = function(data, installations) {
var deviceTokenMap = classifyDeviceTokens(installations, this.validPushTypes);
var sendPromises = [];
for (var pushType in deviceTokenMap) {
var senders = this.getPushSenders(pushType);
// Since ios have dev/prod cert, a push type may have multiple senders
for (var i = 0; i < senders.length; i++) {
var sender = senders[i];
var deviceTokens = deviceTokenMap[pushType];
if (!sender || deviceTokens.length == 0) {
continue;
}
// For android, we can only have 1000 recepients per send
var chunkDeviceTokens = sliceDeviceTokens(pushType, deviceTokens, GCM.GCMRegistrationTokensMax);
for (var j = 0; j < chunkDeviceTokens.length; j++) {
sendPromises.push(sender.send(data, chunkDeviceTokens[j]));
}
}
}
return Parse.Promise.when(sendPromises);
}

/**
* Classify the device token of installations based on its device type.
* @param {Object} installations An array of installations
* @param {Array} validPushTypes An array of valid push types(string)
* @returns {Object} A map whose key is device type and value is an array of device tokens
*/
function classifyDeviceTokens(installations, validPushTypes) {
// Init deviceTokenMap, create a empty array for each valid pushType
var deviceTokenMap = {};
for (var i = 0; i < validPushTypes.length; i++) {
deviceTokenMap[validPushTypes[i]] = [];
}
for (var i = 0; i < installations.length; i++) {
var installation = installations[i];
// No deviceToken, ignore
if (!installation.deviceToken) {
continue;
}
var pushType = installation.deviceType;
if (deviceTokenMap[pushType]) {
deviceTokenMap[pushType].push(installation.deviceToken);
} else {
console.log('Unknown push type from installation %j', installation);
}
}
return deviceTokenMap;
}

/**
* Slice a list of device tokens to several list of device tokens with fixed chunk size.
* @param {String} pushType The push type of the given device tokens
* @param {Array} deviceTokens An array of device tokens(string)
* @param {Number} chunkSize The size of the a chunk
* @returns {Array} An array which contaisn several arries of device tokens with fixed chunk size
*/
function sliceDeviceTokens(pushType, deviceTokens, chunkSize) {
if (pushType !== 'android') {
return [deviceTokens];
}
var chunkDeviceTokens = [];
while (deviceTokens.length > 0) {
chunkDeviceTokens.push(deviceTokens.splice(0, chunkSize));
}
return chunkDeviceTokens;
}

if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
ParsePushAdapter.classifyDeviceTokens = classifyDeviceTokens;
ParsePushAdapter.sliceDeviceTokens = sliceDeviceTokens;
}
module.exports = ParsePushAdapter;
29 changes: 29 additions & 0 deletions PushAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Push Adapter
//
// Allows you to change the push notification mechanism.
//
// Adapter classes must implement the following functions:
// * registerPushSenders(parseConfig)
// * getPushSenders(parseConfig)
// * getValidPushTypes(parseConfig)
// * send(data, installations)
//
// Default is ParsePushAdapter, which uses GCM for
// android push and APNS for ios push.

var ParsePushAdapter = require('./ParsePushAdapter');

var adapter = new ParsePushAdapter();

function setAdapter(pushAdapter) {
adapter = pushAdapter;
}

function getAdapter() {
return adapter;
}

module.exports = {
getAdapter: getAdapter,
setAdapter: setAdapter
};
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var batch = require('./batch'),
express = require('express'),
FilesAdapter = require('./FilesAdapter'),
S3Adapter = require('./S3Adapter'),
PushAdapter = require('./PushAdapter'),
middlewares = require('./middlewares'),
multer = require('multer'),
Parse = require('parse/node').Parse,
Expand Down Expand Up @@ -80,6 +81,10 @@ function ParseServer(args) {
cache.apps[args.appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
}

// Register push senders
var pushConfig = args.push;
PushAdapter.getAdapter().registerPushSenders(pushConfig);

// Initialize the node client SDK automatically
Parse.initialize(args.appId, args.javascriptKey || '', args.masterKey);
if(args.serverURL) {
Expand Down
25 changes: 16 additions & 9 deletions push.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,34 @@

var Parse = require('parse/node').Parse,
PromiseRouter = require('./PromiseRouter'),
PushAdapter = require('./PushAdapter'),
rest = require('./rest');

var validPushTypes = ['ios', 'android'];

function handlePushWithoutQueue(req) {
validateMasterKey(req);
var where = getQueryCondition(req);
validateDeviceType(where);
var pushAdapter = PushAdapter.getAdapter();
validatePushType(where, pushAdapter.getValidPushTypes());
// Replace the expiration_time with a valid Unix epoch milliseconds time
req.body['expiration_time'] = getExpirationTime(req);
return rest.find(req.config, req.auth, '_Installation', where).then(function(response) {
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE,
'This path is not implemented yet.');
// TODO: If the req can pass the checking, we return immediately instead of waiting
// pushes to be sent. We probably change this behaviour in the future.
rest.find(req.config, req.auth, '_Installation', where).then(function(response) {
return pushAdapter.send(req.body, response.results);
});
return Parse.Promise.as({
response: {
'result': true
}
});
}

/**
* Check whether the deviceType parameter in qury condition is valid or not.
* @param {Object} where A query condition
* @param {Array} validPushTypes An array of valid push types(string)
*/
function validateDeviceType(where) {
function validatePushType(where, validPushTypes) {
var where = where || {};
var deviceTypeField = where.deviceType || {};
var deviceTypes = [];
Expand Down Expand Up @@ -113,12 +120,12 @@ var router = new PromiseRouter();
router.route('POST','/push', handlePushWithoutQueue);

module.exports = {
router: router
router: router,
}

if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
module.exports.getQueryCondition = getQueryCondition;
module.exports.validateMasterKey = validateMasterKey;
module.exports.getExpirationTime = getExpirationTime;
module.exports.validateDeviceType = validateDeviceType;
module.exports.validatePushType = validatePushType;
}
Loading

0 comments on commit 681809f

Please sign in to comment.