Skip to content

Commit 681809f

Browse files
committed
Add support for push
1 parent 123ac5f commit 681809f

File tree

8 files changed

+448
-21
lines changed

8 files changed

+448
-21
lines changed

APNS.js

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

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

3842
/**

GCM.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ 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
/**
@@ -39,6 +39,10 @@ GCM.prototype.send = function (data, registrationTokens) {
3939
this.sender.send(message, { registrationTokens: registrationTokens }, 5, function (error, response) {
4040
// TODO: Use the response from gcm to generate and save push report
4141
// TODO: If gcm returns some deviceTokens are invalid, set tombstone for the installation
42+
console.log('GCM request and response %j', {
43+
request: message,
44+
response: response
45+
});
4246
promise.resolve();
4347
});
4448
return promise;
@@ -76,6 +80,8 @@ var generateGCMPayload = function(coreData, pushId, timeStamp, expirationTime) {
7680
return payload;
7781
}
7882

83+
GCM.GCMRegistrationTokensMax = GCMRegistrationTokensMax;
84+
7985
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
8086
GCM.generateGCMPayload = generateGCMPayload;
8187
}

ParsePushAdapter.js

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

0 commit comments

Comments
 (0)