Skip to content

Commit 8f255d6

Browse files
committed
Add support for push
1 parent 123ac5f commit 8f255d6

File tree

10 files changed

+503
-38
lines changed

10 files changed

+503
-38
lines changed

APNS.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,26 @@ function APNS(args) {
3333
});
3434

3535
this.sender.on("socketError", console.error);
36+
37+
this.sender.on("transmitted", function(notification, device) {
38+
console.log("APNS Notification transmitted to:" + device.token.toString("hex"));
39+
});
3640
}
3741

3842
/**
3943
* Send apns request.
4044
* @param {Object} data The data we need to send, the format is the same with api request body
41-
* @param {Array} deviceTokens A array of device tokens
45+
* @param {Array} devices A array of devices
4246
* @returns {Object} A promise which is resolved immediately
4347
*/
44-
APNS.prototype.send = function(data, deviceTokens) {
48+
APNS.prototype.send = function(data, devices) {
4549
var coreData = data.data;
4650
var expirationTime = data['expiration_time'];
4751
var notification = generateNotification(coreData, expirationTime);
52+
var deviceTokens = [];
53+
for (var i = 0; i < devices.length; i++) {
54+
deviceTokens.push(devices[i].deviceToken);
55+
}
4856
this.sender.pushNotification(notification, deviceTokens);
4957
// TODO: pushNotification will push the notification to apn's queue.
5058
// We do not handle error in V1, we just relies apn to auto retry and send the

GCM.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ var randomstring = require('randomstring');
55
var GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
66
var GCMRegistrationTokensMax = 1000;
77

8-
function GCM(apiKey) {
9-
this.sender = new gcm.Sender(apiKey);
8+
function GCM(args) {
9+
this.sender = new gcm.Sender(args.apiKey);
1010
}
1111

1212
/**
1313
* Send gcm request.
1414
* @param {Object} data The data we need to send, the format is the same with api request body
15-
* @param {Array} registrationTokens A array of registration tokens
15+
* @param {Array} devices A array of devices
1616
* @returns {Object} A promise which is resolved after we get results from gcm
1717
*/
18-
GCM.prototype.send = function (data, registrationTokens) {
19-
if (registrationTokens.length >= GCMRegistrationTokensMax) {
18+
GCM.prototype.send = function (data, devices) {
19+
if (devices.length >= GCMRegistrationTokensMax) {
2020
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
2121
'Too many registration tokens for a GCM request.');
2222
}
@@ -36,9 +36,17 @@ GCM.prototype.send = function (data, registrationTokens) {
3636
// Make and send gcm request
3737
var message = new gcm.Message(gcmPayload);
3838
var promise = new Parse.Promise();
39+
var registrationTokens = []
40+
for (var i = 0; i < devices.length; i++) {
41+
registrationTokens.push(devices[i].deviceToken);
42+
}
3943
this.sender.send(message, { registrationTokens: registrationTokens }, 5, function (error, response) {
4044
// TODO: Use the response from gcm to generate and save push report
4145
// TODO: If gcm returns some deviceTokens are invalid, set tombstone for the installation
46+
console.log('GCM request and response %j', {
47+
request: message,
48+
response: response
49+
});
4250
promise.resolve();
4351
});
4452
return promise;
@@ -76,6 +84,8 @@ var generateGCMPayload = function(coreData, pushId, timeStamp, expirationTime) {
7684
return payload;
7785
}
7886

87+
GCM.GCMRegistrationTokensMax = GCMRegistrationTokensMax;
88+
7989
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
8090
GCM.generateGCMPayload = generateGCMPayload;
8191
}

ParsePushAdapter.js

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// ParsePushAdapter is the default implementation of
2+
// PushAdapter, it uses GCM for android push and APNS
3+
// for ios push.
4+
5+
var Parse = require('parse/node').Parse,
6+
GCM = require('./GCM'),
7+
APNS = require('./APNS');
8+
9+
function ParsePushAdapter() {
10+
this.validPushTypes = ['ios', 'android'];
11+
this.senders = {};
12+
}
13+
14+
/**
15+
* Register push senders
16+
* @param {Object} pushConfig The push configuration which is given when parse server is initialized
17+
*/
18+
ParsePushAdapter.prototype.registerPushSenders = function(pushConfig) {
19+
// Initialize senders
20+
for (var i = 0; i < this.validPushTypes.length; i++) {
21+
this.senders[this.validPushTypes[i]] = [];
22+
}
23+
24+
pushConfig = pushConfig || {};
25+
var pushTypes = Object.keys(pushConfig);
26+
for (var i = 0; i < pushTypes.length; i++) {
27+
var pushType = pushTypes[i];
28+
if (this.validPushTypes.indexOf(pushType) < 0) {
29+
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
30+
'Push to ' + pushTypes + ' is not supported');
31+
}
32+
33+
var typePushConfig = pushConfig[pushType];
34+
var senderArgs = [];
35+
// Since for ios, there maybe multiple cert/key pairs,
36+
// typePushConfig can be an array.
37+
if (Array.isArray(typePushConfig)) {
38+
senderArgs = senderArgs.concat(typePushConfig);
39+
} else if (typeof typePushConfig === 'object') {
40+
senderArgs.push(typePushConfig);
41+
} else {
42+
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
43+
'Push Configuration is invalid');
44+
}
45+
for (var j = 0; j < senderArgs.length; j++) {
46+
var senderArg = senderArgs[j];
47+
var sender;
48+
switch (pushType) {
49+
case 'ios':
50+
sender = new APNS(senderArg);
51+
break;
52+
case 'android':
53+
sender = new GCM(senderArg);
54+
break;
55+
}
56+
this.senders[pushType].push(sender);
57+
}
58+
}
59+
}
60+
61+
/**
62+
* Get an array of push senders based on the push type.
63+
* @param {String} The push type
64+
* @returns {Array|Undefined} An array of push senders
65+
*/
66+
ParsePushAdapter.prototype.getPushSenders = function(pushType) {
67+
if (!this.senders[pushType]) {
68+
console.log('No push sender for push type %s', pushType);
69+
return [];
70+
}
71+
return this.senders[pushType];
72+
}
73+
74+
/**
75+
* Get an array of valid push types.
76+
* @returns {Array} An array of valid push types
77+
*/
78+
ParsePushAdapter.prototype.getValidPushTypes = function() {
79+
return this.validPushTypes;
80+
}
81+
82+
ParsePushAdapter.prototype.send = function(data, installations) {
83+
var deviceMap = classifyInstallation(installations, this.validPushTypes);
84+
var sendPromises = [];
85+
for (var pushType in deviceMap) {
86+
var senders = this.getPushSenders(pushType);
87+
// Since ios have dev/prod cert, a push type may have multiple senders
88+
for (var i = 0; i < senders.length; i++) {
89+
var sender = senders[i];
90+
var devices = deviceMap[pushType];
91+
if (!sender || devices.length == 0) {
92+
continue;
93+
}
94+
// For android, we can only have 1000 recepients per send
95+
var chunkDevices = sliceDevices(pushType, devices, GCM.GCMRegistrationTokensMax);
96+
for (var j = 0; j < chunkDevices.length; j++) {
97+
sendPromises.push(sender.send(data, chunkDevices[j]));
98+
}
99+
}
100+
}
101+
return Parse.Promise.when(sendPromises);
102+
}
103+
104+
/**
105+
* Classify the device token of installations based on its device type.
106+
* @param {Object} installations An array of installations
107+
* @param {Array} validPushTypes An array of valid push types(string)
108+
* @returns {Object} A map whose key is device type and value is an array of device
109+
*/
110+
function classifyInstallation(installations, validPushTypes) {
111+
// Init deviceTokenMap, create a empty array for each valid pushType
112+
var deviceMap = {};
113+
for (var i = 0; i < validPushTypes.length; i++) {
114+
deviceMap[validPushTypes[i]] = [];
115+
}
116+
for (var i = 0; i < installations.length; i++) {
117+
var installation = installations[i];
118+
// No deviceToken, ignore
119+
if (!installation.deviceToken) {
120+
continue;
121+
}
122+
var pushType = installation.deviceType;
123+
if (deviceMap[pushType]) {
124+
deviceMap[pushType].push({
125+
deviceToken: installation.deviceToken
126+
});
127+
} else {
128+
console.log('Unknown push type from installation %j', installation);
129+
}
130+
}
131+
return deviceMap;
132+
}
133+
134+
/**
135+
* Slice a list of devices to several list of devices with fixed chunk size.
136+
* @param {String} pushType The push type of the given device tokens
137+
* @param {Array} devices An array of devices
138+
* @param {Number} chunkSize The size of the a chunk
139+
* @returns {Array} An array which contaisn several arries of devices with fixed chunk size
140+
*/
141+
function sliceDevices(pushType, devices, chunkSize) {
142+
if (pushType !== 'android') {
143+
return [devices];
144+
}
145+
var chunkDevices = [];
146+
while (devices.length > 0) {
147+
chunkDevices.push(devices.splice(0, chunkSize));
148+
}
149+
return chunkDevices;
150+
}
151+
152+
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
153+
ParsePushAdapter.classifyInstallation = classifyInstallation;
154+
ParsePushAdapter.sliceDevices = sliceDevices;
155+
}
156+
module.exports = ParsePushAdapter;

PushAdapter.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Push Adapter
2+
//
3+
// Allows you to change the push notification mechanism.
4+
//
5+
// Adapter classes must implement the following functions:
6+
// * registerPushSenders(parseConfig)
7+
// * getPushSenders(parseConfig)
8+
// * getValidPushTypes(parseConfig)
9+
// * send(devices, installations)
10+
//
11+
// Default is ParsePushAdapter, which uses GCM for
12+
// android push and APNS for ios push.
13+
14+
var ParsePushAdapter = require('./ParsePushAdapter');
15+
16+
var adapter = new ParsePushAdapter();
17+
18+
function setAdapter(pushAdapter) {
19+
adapter = pushAdapter;
20+
}
21+
22+
function getAdapter() {
23+
return adapter;
24+
}
25+
26+
module.exports = {
27+
getAdapter: getAdapter,
28+
setAdapter: setAdapter
29+
};

index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ var batch = require('./batch'),
77
express = require('express'),
88
FilesAdapter = require('./FilesAdapter'),
99
S3Adapter = require('./S3Adapter'),
10+
PushAdapter = require('./PushAdapter'),
1011
middlewares = require('./middlewares'),
1112
multer = require('multer'),
1213
Parse = require('parse/node').Parse,
@@ -80,6 +81,10 @@ function ParseServer(args) {
8081
cache.apps[args.appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
8182
}
8283

84+
// Register push senders
85+
var pushConfig = args.push;
86+
PushAdapter.getAdapter().registerPushSenders(pushConfig);
87+
8388
// Initialize the node client SDK automatically
8489
Parse.initialize(args.appId, args.javascriptKey || '', args.masterKey);
8590
if(args.serverURL) {

push.js

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,34 @@
22

33
var Parse = require('parse/node').Parse,
44
PromiseRouter = require('./PromiseRouter'),
5+
PushAdapter = require('./PushAdapter'),
56
rest = require('./rest');
67

7-
var validPushTypes = ['ios', 'android'];
8-
98
function handlePushWithoutQueue(req) {
109
validateMasterKey(req);
1110
var where = getQueryCondition(req);
12-
validateDeviceType(where);
11+
var pushAdapter = PushAdapter.getAdapter();
12+
validatePushType(where, pushAdapter.getValidPushTypes());
1313
// Replace the expiration_time with a valid Unix epoch milliseconds time
1414
req.body['expiration_time'] = getExpirationTime(req);
15-
return rest.find(req.config, req.auth, '_Installation', where).then(function(response) {
16-
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE,
17-
'This path is not implemented yet.');
15+
// TODO: If the req can pass the checking, we return immediately instead of waiting
16+
// pushes to be sent. We probably change this behaviour in the future.
17+
rest.find(req.config, req.auth, '_Installation', where).then(function(response) {
18+
return pushAdapter.send(req.body, response.results);
19+
});
20+
return Parse.Promise.as({
21+
response: {
22+
'result': true
23+
}
1824
});
1925
}
2026

2127
/**
2228
* Check whether the deviceType parameter in qury condition is valid or not.
2329
* @param {Object} where A query condition
30+
* @param {Array} validPushTypes An array of valid push types(string)
2431
*/
25-
function validateDeviceType(where) {
32+
function validatePushType(where, validPushTypes) {
2633
var where = where || {};
2734
var deviceTypeField = where.deviceType || {};
2835
var deviceTypes = [];
@@ -113,12 +120,12 @@ var router = new PromiseRouter();
113120
router.route('POST','/push', handlePushWithoutQueue);
114121

115122
module.exports = {
116-
router: router
123+
router: router,
117124
}
118125

119126
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
120127
module.exports.getQueryCondition = getQueryCondition;
121128
module.exports.validateMasterKey = validateMasterKey;
122129
module.exports.getExpirationTime = getExpirationTime;
123-
module.exports.validateDeviceType = validateDeviceType;
130+
module.exports.validatePushType = validatePushType;
124131
}

spec/APNS.spec.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,18 @@ describe('APNS', () => {
4343
'alert': 'alert'
4444
}
4545
}
46-
// Mock registrationTokens
47-
var deviceTokens = ['token'];
46+
// Mock devices
47+
var devices = [
48+
{ deviceToken: 'token' }
49+
];
4850

49-
var promise = apns.send(data, deviceTokens);
51+
var promise = apns.send(data, devices);
5052
expect(sender.pushNotification).toHaveBeenCalled();
5153
var args = sender.pushNotification.calls.first().args;
5254
var notification = args[0];
5355
expect(notification.alert).toEqual(data.data.alert);
5456
expect(notification.expiry).toEqual(data['expiration_time']);
55-
expect(args[1]).toEqual(deviceTokens);
57+
expect(args[1]).toEqual(['token']);
5658
done();
5759
});
5860
});

0 commit comments

Comments
 (0)