From 6afaeb808b9688d1826b7d0f753478064ec0f908 Mon Sep 17 00:00:00 2001 From: wangmengyan95 Date: Mon, 8 Feb 2016 12:02:07 -0800 Subject: [PATCH] Add support for push --- spec/APNS.spec.js | 10 +- spec/GCM.spec.js | 22 ++- spec/ParsePushAdapter.spec.js | 237 ++++++++++++++++++++++++++ spec/push.spec.js | 25 +-- src/APNS.js | 34 ++-- src/Adapters/Push/ParsePushAdapter.js | 153 +++++++++++++++++ src/Adapters/Push/PushAdapter.js | 29 ++++ src/GCM.js | 54 +++--- src/index.js | 5 + src/push.js | 25 ++- 10 files changed, 530 insertions(+), 64 deletions(-) create mode 100644 spec/ParsePushAdapter.spec.js create mode 100644 src/Adapters/Push/ParsePushAdapter.js create mode 100644 src/Adapters/Push/PushAdapter.js diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index 72490e97..3525fa56 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -43,16 +43,18 @@ describe('APNS', () => { 'alert': 'alert' } } - // Mock registrationTokens - var deviceTokens = ['token']; + // Mock devices + var devices = [ + { deviceToken: 'token' } + ]; - var promise = apns.send(data, deviceTokens); + var promise = apns.send(data, devices); expect(sender.pushNotification).toHaveBeenCalled(); var args = sender.pushNotification.calls.first().args; var notification = args[0]; expect(notification.alert).toEqual(data.data.alert); expect(notification.expiry).toEqual(data['expiration_time']); - expect(args[1]).toEqual(deviceTokens); + expect(args[1]).toEqual(['token']); done(); }); }); diff --git a/spec/GCM.spec.js b/spec/GCM.spec.js index 4bad883e..3e2a7947 100644 --- a/spec/GCM.spec.js +++ b/spec/GCM.spec.js @@ -104,14 +104,18 @@ describe('GCM', () => { 'alert': 'alert' } } - // Mock registrationTokens - var registrationTokens = ['token']; + // Mock devices + var devices = [ + { + deviceToken: 'token' + } + ]; - var promise = gcm.send(data, registrationTokens); + var promise = gcm.send(data, devices); expect(sender.send).toHaveBeenCalled(); var args = sender.send.calls.first().args; // It is too hard to verify message of gcm library, we just verify tokens and retry times - expect(args[1].registrationTokens).toEqual(registrationTokens); + expect(args[1].registrationTokens).toEqual(['token']); expect(args[2]).toEqual(5); done(); }); @@ -123,14 +127,16 @@ describe('GCM', () => { send: jasmine.createSpy('send') }; gcm.sender = sender; - // Mock registrationTokens - var registrationTokens = []; + // Mock devices + var devices = []; for (var i = 0; i <= 2000; i++) { - registrationTokens.push(i.toString()); + devices.push({ + deviceToken: i.toString() + }); } expect(function() { - gcm.send({}, registrationTokens); + gcm.send({}, devices); }).toThrow(); done(); }); diff --git a/spec/ParsePushAdapter.spec.js b/spec/ParsePushAdapter.spec.js new file mode 100644 index 00000000..ca0da190 --- /dev/null +++ b/spec/ParsePushAdapter.spec.js @@ -0,0 +1,237 @@ +var ParsePushAdapter = require('../src/Adapters/Push/ParsePushAdapter'); + +describe('ParsePushAdapter', () => { + it('can be initialized', (done) => { + var parsePushAdapter = new ParsePushAdapter(); + + expect(parsePushAdapter.validPushTypes).toEqual(['ios', 'android']); + done(); + }); + + it('can initialize', (done) => { + var parsePushAdapter = new ParsePushAdapter(); + // Make mock config + var pushConfig = { + android: { + senderId: 'senderId', + apiKey: 'apiKey' + }, + ios: [ + { + cert: 'prodCert.pem', + key: 'prodKey.pem', + production: true + }, + { + cert: 'devCert.pem', + key: 'devKey.pem', + production: false + } + ] + }; + + parsePushAdapter.initialize(pushConfig); + // Check ios + var iosSenders = parsePushAdapter.senders['ios']; + expect(iosSenders.length).toBe(2); + // TODO: Remove this checking onec we inject APNS + var prodApnsOptions = iosSenders[0].sender.options; + expect(prodApnsOptions.cert).toBe(pushConfig.ios[0].cert); + expect(prodApnsOptions.key).toBe(pushConfig.ios[0].key); + expect(prodApnsOptions.production).toBe(pushConfig.ios[0].production); + var devApnsOptions = iosSenders[1].sender.options; + expect(devApnsOptions.cert).toBe(pushConfig.ios[1].cert); + expect(devApnsOptions.key).toBe(pushConfig.ios[1].key); + expect(devApnsOptions.production).toBe(pushConfig.ios[1].production); + // Check android + var androidSenders = parsePushAdapter.senders['android']; + expect(androidSenders.length).toBe(1); + var androidSender = androidSenders[0]; + // TODO: Remove this checking onec we inject GCM + expect(androidSender.sender.key).toBe(pushConfig.android.apiKey); + done(); + }); + + it('can throw on initializing with unsupported push type', (done) => { + var parsePushAdapter = new ParsePushAdapter(); + // Make mock config + var pushConfig = { + win: { + senderId: 'senderId', + apiKey: 'apiKey' + } + }; + + expect(function() { + parsePushAdapter.initialize(pushConfig) + }).toThrow(); + done(); + }); + + it('can throw on initializing with invalid pushConfig', (done) => { + var parsePushAdapter = new ParsePushAdapter(); + // Make mock config + var pushConfig = { + android: 123 + }; + + expect(function() { + parsePushAdapter.initialize(pushConfig) + }).toThrow(); + done(); + }); + + it('can get push senders', (done) => { + var parsePushAdapter = new ParsePushAdapter(); + // Mock push senders + var androidSender = {}; + var iosSender = {}; + var iosSenderAgain = {}; + parsePushAdapter.senders = { + android: [ + androidSender + ], + ios: [ + iosSender, + iosSenderAgain + ] + }; + + expect(parsePushAdapter.getPushSenders('android')).toEqual([androidSender]); + expect(parsePushAdapter.getPushSenders('ios')).toEqual([iosSender, iosSenderAgain]); + done(); + }); + + it('can get empty push senders', (done) => { + var parsePushAdapter = new ParsePushAdapter(); + + expect(parsePushAdapter.getPushSenders('android')).toEqual([]); + done(); + }); + + it('can get valid push types', (done) => { + var parsePushAdapter = new ParsePushAdapter(); + + expect(parsePushAdapter.getValidPushTypes()).toEqual(['ios', 'android']); + done(); + }); + + it('can classify installation', (done) => { + // Mock installations + var validPushTypes = ['ios', 'android']; + var installations = [ + { + deviceType: 'android', + deviceToken: 'androidToken' + }, + { + deviceType: 'ios', + deviceToken: 'iosToken' + }, + { + deviceType: 'win', + deviceToken: 'winToken' + }, + { + deviceType: 'android', + deviceToken: undefined + } + ]; + + var deviceTokenMap = ParsePushAdapter.classifyInstallation(installations, validPushTypes); + expect(deviceTokenMap['android']).toEqual([makeDevice('androidToken')]); + expect(deviceTokenMap['ios']).toEqual([makeDevice('iosToken')]); + expect(deviceTokenMap['win']).toBe(undefined); + done(); + }); + + it('can slice ios devices', (done) => { + // Mock devices + var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)]; + + var chunkDevices = ParsePushAdapter.sliceDevices('ios', devices, 2); + expect(chunkDevices).toEqual([devices]); + done(); + }); + + it('can slice android devices', (done) => { + // Mock devices + var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)]; + + var chunkDevices = ParsePushAdapter.sliceDevices('android', devices, 3); + expect(chunkDevices).toEqual([ + [makeDevice(1), makeDevice(2), makeDevice(3)], + [makeDevice(4)] + ]); + done(); + }); + + + it('can send push notifications', (done) => { + var parsePushAdapter = new ParsePushAdapter(); + // Mock android ios senders + var androidSender = { + send: jasmine.createSpy('send') + }; + var iosSender = { + send: jasmine.createSpy('send') + }; + var iosSenderAgain = { + send: jasmine.createSpy('send') + }; + var senders = { + ios: [iosSender, iosSenderAgain], + android: [androidSender] + }; + parsePushAdapter.senders = senders; + // Mock installations + var installations = [ + { + deviceType: 'android', + deviceToken: 'androidToken' + }, + { + deviceType: 'ios', + deviceToken: 'iosToken' + }, + { + deviceType: 'win', + deviceToken: 'winToken' + }, + { + deviceType: 'android', + deviceToken: undefined + } + ]; + var data = {}; + + parsePushAdapter.send(data, installations); + // Check android sender + expect(androidSender.send).toHaveBeenCalled(); + var args = androidSender.send.calls.first().args; + expect(args[0]).toEqual(data); + expect(args[1]).toEqual([ + makeDevice('androidToken') + ]); + // Check ios sender + expect(iosSender.send).toHaveBeenCalled(); + args = iosSender.send.calls.first().args; + expect(args[0]).toEqual(data); + expect(args[1]).toEqual([ + makeDevice('iosToken') + ]); + expect(iosSenderAgain.send).toHaveBeenCalled(); + args = iosSenderAgain.send.calls.first().args; + expect(args[0]).toEqual(data); + expect(args[1]).toEqual([ + makeDevice('iosToken') + ]); + done(); + }); + + function makeDevice(deviceToken) { + return { + deviceToken: deviceToken + }; + } +}); diff --git a/spec/push.spec.js b/spec/push.spec.js index a2ea41b5..ba7588f3 100644 --- a/spec/push.spec.js +++ b/spec/push.spec.js @@ -104,10 +104,11 @@ describe('push', () => { it('can validate device type when no device type is set', (done) => { // Make query condition var where = { - } + }; + var validPushTypes = ['ios', 'android']; expect(function(){ - push.validateDeviceType(where); + push.validatePushType(where, validPushTypes); }).not.toThrow(); done(); }); @@ -116,10 +117,11 @@ describe('push', () => { // Make query condition var where = { 'deviceType': 'ios' - } + }; + var validPushTypes = ['ios', 'android']; expect(function(){ - push.validateDeviceType(where); + push.validatePushType(where, validPushTypes); }).not.toThrow(); done(); }); @@ -130,10 +132,11 @@ describe('push', () => { 'deviceType': { '$in': ['android', 'ios'] } - } + }; + var validPushTypes = ['ios', 'android']; expect(function(){ - push.validateDeviceType(where); + push.validatePushType(where, validPushTypes); }).not.toThrow(); done(); }); @@ -142,10 +145,11 @@ describe('push', () => { // Make query condition var where = { 'deviceType': 'osx' - } + }; + var validPushTypes = ['ios', 'android']; expect(function(){ - push.validateDeviceType(where); + push.validatePushType(where, validPushTypes); }).toThrow(); done(); }); @@ -154,10 +158,11 @@ describe('push', () => { // Make query condition var where = { 'deviceType': 'osx' - } + }; + var validPushTypes = ['ios', 'android']; expect(function(){ - push.validateDeviceType(where) + push.validatePushType(where, validPushTypes); }).toThrow(); done(); }); diff --git a/src/APNS.js b/src/APNS.js index 85c97401..e6275369 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -1,7 +1,9 @@ -var Parse = require('parse/node').Parse; +"use strict"; + +const Parse = require('parse/node').Parse; // TODO: apn does not support the new HTTP/2 protocal. It is fine to use it in V1, // but probably we will replace it in the future. -var apn = require('apn'); +const apn = require('apn'); /** * Create a new connection to the APN service. @@ -33,18 +35,26 @@ function APNS(args) { }); this.sender.on("socketError", console.error); + + this.sender.on("transmitted", function(notification, device) { + console.log("APNS Notification transmitted to:" + device.token.toString("hex")); + }); } /** * Send apns request. * @param {Object} data The data we need to send, the format is the same with api request body - * @param {Array} deviceTokens A array of device tokens + * @param {Array} devices A array of devices * @returns {Object} A promise which is resolved immediately */ -APNS.prototype.send = function(data, deviceTokens) { - var coreData = data.data; - var expirationTime = data['expiration_time']; - var notification = generateNotification(coreData, expirationTime); +APNS.prototype.send = function(data, devices) { + let coreData = data.data; + let expirationTime = data['expiration_time']; + let notification = generateNotification(coreData, expirationTime); + let deviceTokens = []; + for (let device of devices) { + deviceTokens.push(device.deviceToken); + } this.sender.pushNotification(notification, deviceTokens); // TODO: pushNotification will push the notification to apn's queue. // We do not handle error in V1, we just relies apn to auto retry and send the @@ -57,10 +67,10 @@ APNS.prototype.send = function(data, deviceTokens) { * @param {Object} coreData The data field under api request body * @returns {Object} A apns notification */ -var generateNotification = function(coreData, expirationTime) { - var notification = new apn.notification(); - var payload = {}; - for (var key in coreData) { +let generateNotification = function(coreData, expirationTime) { + let notification = new apn.notification(); + let payload = {}; + for (let key in coreData) { switch (key) { case 'alert': notification.setAlertText(coreData.alert); @@ -73,7 +83,7 @@ var generateNotification = function(coreData, expirationTime) { break; case 'content-available': notification.setNewsstandAvailable(true); - var isAvailable = coreData['content-available'] === 1; + let isAvailable = coreData['content-available'] === 1; notification.setContentAvailable(isAvailable); break; case 'category': diff --git a/src/Adapters/Push/ParsePushAdapter.js b/src/Adapters/Push/ParsePushAdapter.js new file mode 100644 index 00000000..55b99032 --- /dev/null +++ b/src/Adapters/Push/ParsePushAdapter.js @@ -0,0 +1,153 @@ +"use strict"; +// ParsePushAdapter is the default implementation of +// PushAdapter, it uses GCM for android push and APNS +// for ios push. + +const Parse = require('parse/node').Parse; +const GCM = require('../../GCM'); +const APNS = require('../../APNS'); + +function ParsePushAdapter() { + this.validPushTypes = ['ios', 'android']; + this.senders = {}; +} + +/** + * Register push senders + * @param {Object} pushConfig The push configuration which is given when parse server is initialized + */ +ParsePushAdapter.prototype.initialize = function(pushConfig) { + // Initialize senders + for (let validPushType of this.validPushTypes) { + this.senders[validPushType] = []; + } + + pushConfig = pushConfig || {}; + let pushTypes = Object.keys(pushConfig); + for (let pushType of pushTypes) { + if (this.validPushTypes.indexOf(pushType) < 0) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Push to ' + pushTypes + ' is not supported'); + } + + let typePushConfig = pushConfig[pushType]; + let 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 (let senderArg of senderArgs) { + let 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) { + let deviceMap = classifyInstallation(installations, this.validPushTypes); + let sendPromises = []; + for (let pushType in deviceMap) { + let senders = this.getPushSenders(pushType); + // Since ios have dev/prod cert, a push type may have multiple senders + for (let sender of senders) { + let devices = deviceMap[pushType]; + if (!sender || devices.length == 0) { + continue; + } + // For android, we can only have 1000 recepients per send + let chunkDevices = sliceDevices(pushType, devices, GCM.GCMRegistrationTokensMax); + for (let chunkDevice of chunkDevices) { + sendPromises.push(sender.send(data, chunkDevice)); + } + } + } + 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 + */ +function classifyInstallation(installations, validPushTypes) { + // Init deviceTokenMap, create a empty array for each valid pushType + let deviceMap = {}; + for (let validPushType of validPushTypes) { + deviceMap[validPushType] = []; + } + for (let installation of installations) { + // No deviceToken, ignore + if (!installation.deviceToken) { + continue; + } + let pushType = installation.deviceType; + if (deviceMap[pushType]) { + deviceMap[pushType].push({ + deviceToken: installation.deviceToken + }); + } else { + console.log('Unknown push type from installation %j', installation); + } + } + return deviceMap; +} + +/** + * Slice a list of devices to several list of devices with fixed chunk size. + * @param {String} pushType The push type of the given device tokens + * @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(pushType, devices, chunkSize) { + if (pushType !== 'android') { + return [devices]; + } + let chunkDevices = []; + while (devices.length > 0) { + chunkDevices.push(devices.splice(0, chunkSize)); + } + return chunkDevices; +} + +if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { + ParsePushAdapter.classifyInstallation = classifyInstallation; + ParsePushAdapter.sliceDevices = sliceDevices; +} +module.exports = ParsePushAdapter; diff --git a/src/Adapters/Push/PushAdapter.js b/src/Adapters/Push/PushAdapter.js new file mode 100644 index 00000000..ab2f7133 --- /dev/null +++ b/src/Adapters/Push/PushAdapter.js @@ -0,0 +1,29 @@ +// Push Adapter +// +// Allows you to change the push notification mechanism. +// +// Adapter classes must implement the following functions: +// * initialize(pushConfig) +// * getPushSenders(parseConfig) +// * getValidPushTypes(parseConfig) +// * send(devices, 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 +}; diff --git a/src/GCM.js b/src/GCM.js index b9d5c728..9dfe1b06 100644 --- a/src/GCM.js +++ b/src/GCM.js @@ -1,44 +1,54 @@ -var Parse = require('parse/node').Parse; -var gcm = require('node-gcm'); -var randomstring = require('randomstring'); +"use strict"; -var GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks -var GCMRegistrationTokensMax = 1000; +const Parse = require('parse/node').Parse; +const gcm = require('node-gcm'); +const randomstring = require('randomstring'); -function GCM(apiKey) { - this.sender = new gcm.Sender(apiKey); +const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks +const GCMRegistrationTokensMax = 1000; + +function GCM(args) { + 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} registrationTokens A array of registration tokens + * @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, registrationTokens) { - if (registrationTokens.length >= GCMRegistrationTokensMax) { +GCM.prototype.send = function(data, devices) { + if (devices.length >= GCMRegistrationTokensMax) { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Too many registration tokens for a GCM request.'); } - var pushId = randomstring.generate({ + let pushId = randomstring.generate({ length: 10, charset: 'alphanumeric' }); - var timeStamp = Date.now(); - var expirationTime; + let timeStamp = Date.now(); + 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 - var gcmPayload = generateGCMPayload(data.data, pushId, timeStamp, expirationTime); + let gcmPayload = generateGCMPayload(data.data, pushId, timeStamp, expirationTime); // Make and send gcm request - var message = new gcm.Message(gcmPayload); - var promise = new Parse.Promise(); - this.sender.send(message, { registrationTokens: registrationTokens }, 5, function (error, response) { + let message = new gcm.Message(gcmPayload); + let promise = new Parse.Promise(); + let registrationTokens = [] + for (let device of devices) { + registrationTokens.push(device.deviceToken); + } + this.sender.send(message, { registrationTokens: registrationTokens }, 5, (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; @@ -52,19 +62,19 @@ GCM.prototype.send = function (data, registrationTokens) { * @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 */ -var generateGCMPayload = function(coreData, pushId, timeStamp, expirationTime) { - var payloadData = { +let generateGCMPayload = function(coreData, pushId, timeStamp, expirationTime) { + let payloadData = { 'time': new Date(timeStamp).toISOString(), 'push_id': pushId, 'data': JSON.stringify(coreData) } - var payload = { + let payload = { priority: 'normal', data: payloadData }; if (expirationTime) { // The timeStamp and expiration is in milliseconds but gcm requires second - var timeToLive = Math.floor((expirationTime - timeStamp) / 1000); + let timeToLive = Math.floor((expirationTime - timeStamp) / 1000); if (timeToLive < 0) { timeToLive = 0; } @@ -76,6 +86,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; } diff --git a/src/index.js b/src/index.js index ef29ec7f..1bc02dc7 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ var batch = require('./batch'), cache = require('./cache'), DatabaseAdapter = require('./DatabaseAdapter'), express = require('express'), + PushAdapter = require('./Adapters/Push/PushAdapter'), middlewares = require('./middlewares'), multer = require('multer'), Parse = require('parse/node').Parse, @@ -86,6 +87,10 @@ function ParseServer(args) { cache.apps[args.appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } + // Register push senders + var pushConfig = args.push; + PushAdapter.getAdapter().initialize(pushConfig); + // Initialize the node client SDK automatically Parse.initialize(args.appId, args.javascriptKey || '', args.masterKey); if(args.serverURL) { diff --git a/src/push.js b/src/push.js index 29a6a944..013b85d4 100644 --- a/src/push.js +++ b/src/push.js @@ -2,27 +2,34 @@ var Parse = require('parse/node').Parse, PromiseRouter = require('./PromiseRouter'), + PushAdapter = require('./Adapters/Push/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 = []; @@ -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; }