Change APNS multiple certs handling
This commit is contained in:
@@ -1,6 +1,65 @@
|
||||
var APNS = require('../src/APNS');
|
||||
|
||||
describe('APNS', () => {
|
||||
|
||||
it('can initialize with single cert', (done) => {
|
||||
var args = {
|
||||
cert: 'prodCert.pem',
|
||||
key: 'prodKey.pem',
|
||||
production: true,
|
||||
bundleId: 'bundleId'
|
||||
}
|
||||
var apns = new APNS(args);
|
||||
|
||||
expect(apns.conns.length).toBe(1);
|
||||
var apnsConnection = apns.conns[0];
|
||||
expect(apnsConnection.index).toBe(0);
|
||||
expect(apnsConnection.bundleId).toBe(args.bundleId);
|
||||
// TODO: Remove this checking onec we inject APNS
|
||||
var prodApnsOptions = apnsConnection.options;
|
||||
expect(prodApnsOptions.cert).toBe(args.cert);
|
||||
expect(prodApnsOptions.key).toBe(args.key);
|
||||
expect(prodApnsOptions.production).toBe(args.production);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can initialize with multiple certs', (done) => {
|
||||
var args = [
|
||||
{
|
||||
cert: 'devCert.pem',
|
||||
key: 'devKey.pem',
|
||||
production: false,
|
||||
bundleId: 'bundleId'
|
||||
},
|
||||
{
|
||||
cert: 'prodCert.pem',
|
||||
key: 'prodKey.pem',
|
||||
production: true,
|
||||
bundleId: 'bundleIdAgain'
|
||||
}
|
||||
]
|
||||
|
||||
var apns = new APNS(args);
|
||||
expect(apns.conns.length).toBe(2);
|
||||
var devApnsConnection = apns.conns[1];
|
||||
expect(devApnsConnection.index).toBe(1);
|
||||
var devApnsOptions = devApnsConnection.options;
|
||||
expect(devApnsOptions.cert).toBe(args[0].cert);
|
||||
expect(devApnsOptions.key).toBe(args[0].key);
|
||||
expect(devApnsOptions.production).toBe(args[0].production);
|
||||
expect(devApnsConnection.bundleId).toBe(args[0].bundleId);
|
||||
|
||||
var prodApnsConnection = apns.conns[0];
|
||||
expect(prodApnsConnection.index).toBe(0);
|
||||
// TODO: Remove this checking onec we inject APNS
|
||||
var prodApnsOptions = prodApnsConnection.options;
|
||||
expect(prodApnsOptions.cert).toBe(args[1].cert);
|
||||
expect(prodApnsOptions.key).toBe(args[1].key);
|
||||
expect(prodApnsOptions.production).toBe(args[1].production);
|
||||
expect(prodApnsOptions.bundleId).toBe(args[1].bundleId);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can generate APNS notification', (done) => {
|
||||
//Mock request data
|
||||
var data = {
|
||||
@@ -29,12 +88,195 @@ describe('APNS', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can send APNS notification', (done) => {
|
||||
var apns = new APNS();
|
||||
var sender = {
|
||||
pushNotification: jasmine.createSpy('send')
|
||||
it('can choose conns for device without appIdentifier', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
bundleId: 'bundleId'
|
||||
},
|
||||
{
|
||||
bundleId: 'bundleIdAgain'
|
||||
}
|
||||
];
|
||||
// Mock device
|
||||
var device = {};
|
||||
|
||||
var qualifiedConns = APNS.chooseConns(conns, device);
|
||||
expect(qualifiedConns).toEqual([0, 1]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can choose conns for device with valid appIdentifier', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
bundleId: 'bundleId'
|
||||
},
|
||||
{
|
||||
bundleId: 'bundleIdAgain'
|
||||
}
|
||||
];
|
||||
// Mock device
|
||||
var device = {
|
||||
appIdentifier: 'bundleId'
|
||||
};
|
||||
apns.sender = sender;
|
||||
|
||||
var qualifiedConns = APNS.chooseConns(conns, device);
|
||||
expect(qualifiedConns).toEqual([0]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can choose conns for device with invalid appIdentifier', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
bundleId: 'bundleId'
|
||||
},
|
||||
{
|
||||
bundleId: 'bundleIdAgain'
|
||||
}
|
||||
];
|
||||
// Mock device
|
||||
var device = {
|
||||
appIdentifier: 'invalid'
|
||||
};
|
||||
|
||||
var qualifiedConns = APNS.chooseConns(conns, device);
|
||||
expect(qualifiedConns).toEqual([]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle transmission error when notification is not in cache or device is missing', (done) => {
|
||||
// Mock conns
|
||||
var conns = [];
|
||||
var errorCode = 1;
|
||||
var notification = undefined;
|
||||
var device = {};
|
||||
|
||||
APNS.handleTransmissionError(conns, errorCode, notification, device);
|
||||
|
||||
var notification = {};
|
||||
var device = undefined;
|
||||
|
||||
APNS.handleTransmissionError(conns, errorCode, notification, device);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle transmission error when there are other qualified conns', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId2'
|
||||
},
|
||||
];
|
||||
var errorCode = 1;
|
||||
var notification = {};
|
||||
var apnDevice = {
|
||||
connIndex: 0,
|
||||
appIdentifier: 'bundleId1'
|
||||
};
|
||||
|
||||
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
|
||||
|
||||
expect(conns[0].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[1].pushNotification).toHaveBeenCalled();
|
||||
expect(conns[2].pushNotification).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle transmission error when there is no other qualified conns', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId2'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
}
|
||||
];
|
||||
var errorCode = 1;
|
||||
var notification = {};
|
||||
var apnDevice = {
|
||||
connIndex: 2,
|
||||
appIdentifier: 'bundleId1'
|
||||
};
|
||||
|
||||
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
|
||||
|
||||
expect(conns[0].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[1].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[2].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[3].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[4].pushNotification).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle transmission error when device has no appIdentifier', (done) => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId1'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId2'
|
||||
},
|
||||
{
|
||||
pushNotification: jasmine.createSpy('pushNotification'),
|
||||
bundleId: 'bundleId3'
|
||||
},
|
||||
];
|
||||
var errorCode = 1;
|
||||
var notification = {};
|
||||
var apnDevice = {
|
||||
connIndex: 1,
|
||||
};
|
||||
|
||||
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
|
||||
|
||||
expect(conns[0].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[1].pushNotification).not.toHaveBeenCalled();
|
||||
expect(conns[2].pushNotification).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can send APNS notification', (done) => {
|
||||
var args = {
|
||||
cert: 'prodCert.pem',
|
||||
key: 'prodKey.pem',
|
||||
production: true,
|
||||
bundleId: 'bundleId'
|
||||
}
|
||||
var apns = new APNS(args);
|
||||
var conn = {
|
||||
pushNotification: jasmine.createSpy('send'),
|
||||
bundleId: 'bundleId'
|
||||
};
|
||||
apns.conns = [ conn ];
|
||||
// Mock data
|
||||
var expirationTime = 1454571491354
|
||||
var data = {
|
||||
@@ -45,16 +287,21 @@ describe('APNS', () => {
|
||||
}
|
||||
// Mock devices
|
||||
var devices = [
|
||||
{ deviceToken: 'token' }
|
||||
{
|
||||
deviceToken: '112233',
|
||||
appIdentifier: 'bundleId'
|
||||
}
|
||||
];
|
||||
|
||||
var promise = apns.send(data, devices);
|
||||
expect(sender.pushNotification).toHaveBeenCalled();
|
||||
var args = sender.pushNotification.calls.first().args;
|
||||
expect(conn.pushNotification).toHaveBeenCalled();
|
||||
var args = conn.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(['token']);
|
||||
var apnDevice = args[1]
|
||||
expect(apnDevice.connIndex).toEqual(0);
|
||||
expect(apnDevice.appIdentifier).toEqual('bundleId');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
var GCM = require('../src/GCM');
|
||||
|
||||
describe('GCM', () => {
|
||||
it('can initialize', (done) => {
|
||||
var args = {
|
||||
apiKey: 'apiKey'
|
||||
};
|
||||
var gcm = new GCM(args);
|
||||
expect(gcm.sender.key).toBe(args.apiKey);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can throw on initializing with invalid args', (done) => {
|
||||
var args = 123
|
||||
expect(function() {
|
||||
new GCM(args);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can generate GCM Payload without expiration time', (done) => {
|
||||
//Mock request data
|
||||
var data = {
|
||||
@@ -90,7 +107,9 @@ describe('GCM', () => {
|
||||
});
|
||||
|
||||
it('can send GCM request', (done) => {
|
||||
var gcm = new GCM('apiKey');
|
||||
var gcm = new GCM({
|
||||
apiKey: 'apiKey'
|
||||
});
|
||||
// Mock gcm sender
|
||||
var sender = {
|
||||
send: jasmine.createSpy('send')
|
||||
@@ -111,7 +130,7 @@ describe('GCM', () => {
|
||||
}
|
||||
];
|
||||
|
||||
var promise = gcm.send(data, devices);
|
||||
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
|
||||
@@ -120,24 +139,21 @@ describe('GCM', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can throw on sending when we have too many registration tokens', (done) => {
|
||||
var gcm = new GCM('apiKey');
|
||||
// Mock gcm sender
|
||||
var sender = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
gcm.sender = sender;
|
||||
it('can slice devices', (done) => {
|
||||
// Mock devices
|
||||
var devices = [];
|
||||
for (var i = 0; i <= 2000; i++) {
|
||||
devices.push({
|
||||
deviceToken: i.toString()
|
||||
});
|
||||
}
|
||||
var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)];
|
||||
|
||||
expect(function() {
|
||||
gcm.send({}, devices);
|
||||
}).toThrow();
|
||||
var chunkDevices = GCM.sliceDevices(devices, 3);
|
||||
expect(chunkDevices).toEqual([
|
||||
[makeDevice(1), makeDevice(2), makeDevice(3)],
|
||||
[makeDevice(4)]
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
function makeDevice(deviceToken) {
|
||||
return {
|
||||
deviceToken: deviceToken
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
var ParsePushAdapter = require('../src/Adapters/Push/ParsePushAdapter');
|
||||
var APNS = require('../src/APNS');
|
||||
var GCM = require('../src/GCM');
|
||||
|
||||
describe('ParsePushAdapter', () => {
|
||||
it('can be initialized', (done) => {
|
||||
@@ -12,35 +14,25 @@ describe('ParsePushAdapter', () => {
|
||||
{
|
||||
cert: 'prodCert.pem',
|
||||
key: 'prodKey.pem',
|
||||
production: true
|
||||
production: true,
|
||||
bundleId: 'bundleId'
|
||||
},
|
||||
{
|
||||
cert: 'devCert.pem',
|
||||
key: 'devKey.pem',
|
||||
production: false
|
||||
production: false,
|
||||
bundleId: 'bundleIdAgain'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var parsePushAdapter = new ParsePushAdapter(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);
|
||||
var iosSender = parsePushAdapter.senderMap['ios'];
|
||||
expect(iosSender instanceof APNS).toBe(true);
|
||||
// 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);
|
||||
var androidSender = parsePushAdapter.senderMap['android'];
|
||||
expect(androidSender instanceof GCM).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -59,46 +51,6 @@ describe('ParsePushAdapter', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can throw on initializing with invalid pushConfig', (done) => {
|
||||
// Make mock config
|
||||
var pushConfig = {
|
||||
android: 123
|
||||
};
|
||||
|
||||
expect(function() {
|
||||
new ParsePushAdapter(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();
|
||||
|
||||
@@ -128,31 +80,10 @@ describe('ParsePushAdapter', () => {
|
||||
}
|
||||
];
|
||||
|
||||
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)]
|
||||
]);
|
||||
var deviceMap = ParsePushAdapter.classifyInstallation(installations, validPushTypes);
|
||||
expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
|
||||
expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
|
||||
expect(deviceMap['win']).toBe(undefined);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -166,14 +97,11 @@ describe('ParsePushAdapter', () => {
|
||||
var iosSender = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
var iosSenderAgain = {
|
||||
send: jasmine.createSpy('send')
|
||||
var senderMap = {
|
||||
ios: iosSender,
|
||||
android: androidSender
|
||||
};
|
||||
var senders = {
|
||||
ios: [iosSender, iosSenderAgain],
|
||||
android: [androidSender]
|
||||
};
|
||||
parsePushAdapter.senders = senders;
|
||||
parsePushAdapter.senderMap = senderMap;
|
||||
// Mock installations
|
||||
var installations = [
|
||||
{
|
||||
@@ -210,18 +138,13 @@ describe('ParsePushAdapter', () => {
|
||||
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) {
|
||||
function makeDevice(deviceToken, appIdentifier) {
|
||||
return {
|
||||
deviceToken: deviceToken
|
||||
deviceToken: deviceToken,
|
||||
appIdentifier: appIdentifier
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
167
src/APNS.js
167
src/APNS.js
@@ -8,37 +8,77 @@ const apn = require('apn');
|
||||
/**
|
||||
* Create a new connection to the APN service.
|
||||
* @constructor
|
||||
* @param {Object} args Arguments to config APNS connection
|
||||
* @param {String} args.cert The filename of the connection certificate to load from disk, default is cert.pem
|
||||
* @param {String} args.key The filename of the connection key to load from disk, default is key.pem
|
||||
* @param {Object|Array} args An argument or a list of arguments to config APNS connection
|
||||
* @param {String} args.cert The filename of the connection certificate to load from disk
|
||||
* @param {String} args.key The filename of the connection key to load from disk
|
||||
* @param {String} args.pfx The filename for private key, certificate and CA certs in PFX or PKCS12 format, it will overwrite cert and key
|
||||
* @param {String} args.passphrase The passphrase for the connection key, if required
|
||||
* @param {String} args.bundleId The bundleId for cert
|
||||
* @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox
|
||||
*/
|
||||
function APNS(args) {
|
||||
this.sender = new apn.connection(args);
|
||||
// Since for ios, there maybe multiple cert/key pairs,
|
||||
// typePushConfig can be an array.
|
||||
let apnsArgsList = [];
|
||||
if (Array.isArray(args)) {
|
||||
apnsArgsList = apnsArgsList.concat(args);
|
||||
} else if (typeof args === 'object') {
|
||||
apnsArgsList.push(args);
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'APNS Configuration is invalid');
|
||||
}
|
||||
|
||||
this.sender.on('connected', function() {
|
||||
console.log('APNS Connected');
|
||||
});
|
||||
|
||||
this.sender.on('transmissionError', function(errCode, notification, device) {
|
||||
console.error('APNS Notification caused error: ' + errCode + ' for device ', device, notification);
|
||||
// TODO: For error caseud by invalid deviceToken, we should mark those installations.
|
||||
});
|
||||
|
||||
this.sender.on("timeout", function () {
|
||||
console.log("APNS Connection Timeout");
|
||||
});
|
||||
|
||||
this.sender.on("disconnected", function() {
|
||||
console.log("APNS Disconnected");
|
||||
});
|
||||
|
||||
this.sender.on("socketError", console.error);
|
||||
|
||||
this.sender.on("transmitted", function(notification, device) {
|
||||
console.log("APNS Notification transmitted to:" + device.token.toString("hex"));
|
||||
this.conns = [];
|
||||
for (let apnsArgs of apnsArgsList) {
|
||||
let conn = new apn.Connection(apnsArgs);
|
||||
if (!apnsArgs.bundleId) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'BundleId is mssing for %j', apnsArgs);
|
||||
}
|
||||
conn.bundleId = apnsArgs.bundleId;
|
||||
// Set the priority of the conns, prod cert has higher priority
|
||||
if (apnsArgs.production) {
|
||||
conn.priority = 0;
|
||||
} else {
|
||||
conn.priority = 1;
|
||||
}
|
||||
|
||||
// Set apns client callbacks
|
||||
conn.on('connected', () => {
|
||||
console.log('APNS Connection %d Connected', conn.index);
|
||||
});
|
||||
|
||||
conn.on('transmissionError', (errCode, notification, apnDevice) => {
|
||||
handleTransmissionError(this.conns, errCode, notification, apnDevice);
|
||||
});
|
||||
|
||||
conn.on('timeout', () => {
|
||||
console.log('APNS Connection %d Timeout', conn.index);
|
||||
});
|
||||
|
||||
conn.on('disconnected', () => {
|
||||
console.log('APNS Connection %d Disconnected', conn.index);
|
||||
});
|
||||
|
||||
conn.on('socketError', () => {
|
||||
console.log('APNS Connection %d Socket Error', conn.index);
|
||||
});
|
||||
|
||||
conn.on('transmitted', function(notification, device) {
|
||||
console.log('APNS Connection %d Notification transmitted to %s', conn.index, device.token.toString('hex'));
|
||||
});
|
||||
|
||||
this.conns.push(conn);
|
||||
}
|
||||
// Sort the conn based on priority ascending, high pri first
|
||||
this.conns.sort((s1, s2) => {
|
||||
return s1.priority - s2.priority;
|
||||
});
|
||||
// Set index of conns
|
||||
for (let index = 0; index < this.conns.length; index++) {
|
||||
this.conns[index].index = index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,23 +91,84 @@ 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);
|
||||
let qualifiedConnIndexs = chooseConns(this.conns, device);
|
||||
// We can not find a valid conn, just ignore this device
|
||||
if (qualifiedConnIndexs.length == 0) {
|
||||
continue;
|
||||
}
|
||||
let conn = this.conns[qualifiedConnIndexs[0]];
|
||||
let apnDevice = new apn.Device(device.deviceToken);
|
||||
apnDevice.connIndex = qualifiedConnIndexs[0];
|
||||
// Add additional appIdentifier info to apn device instance
|
||||
if (device.appIdentifier) {
|
||||
apnDevice.appIdentifier = device.appIdentifier;
|
||||
}
|
||||
conn.pushNotification(notification, apnDevice);
|
||||
}
|
||||
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
|
||||
// notifications.
|
||||
return Parse.Promise.as();
|
||||
}
|
||||
|
||||
function handleTransmissionError(conns, errCode, notification, apnDevice) {
|
||||
console.error('APNS Notification caused error: ' + errCode + ' for device ', apnDevice, notification);
|
||||
// This means the error notification is not in the cache anymore or the recepient is missing,
|
||||
// we just ignore this case
|
||||
if (!notification || !apnDevice) {
|
||||
return
|
||||
}
|
||||
|
||||
// If currentConn can not send the push notification, we try to use the next available conn.
|
||||
// Since conns is sorted by priority, the next conn means the next low pri conn.
|
||||
// If there is no conn available, we give up on sending the notification to that device.
|
||||
let qualifiedConnIndexs = chooseConns(conns, apnDevice);
|
||||
let currentConnIndex = apnDevice.connIndex;
|
||||
|
||||
let newConnIndex = -1;
|
||||
// Find the next element of currentConnIndex in qualifiedConnIndexs
|
||||
for (let index = 0; index < qualifiedConnIndexs.length - 1; index++) {
|
||||
if (qualifiedConnIndexs[index] === currentConnIndex) {
|
||||
newConnIndex = qualifiedConnIndexs[index + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// There is no more available conns, we give up in this case
|
||||
if (newConnIndex < 0 || newConnIndex >= conns.length) {
|
||||
console.log('APNS can not find vaild connection for %j', apnDevice.token);
|
||||
return;
|
||||
}
|
||||
|
||||
let newConn = conns[newConnIndex];
|
||||
// Update device conn info
|
||||
apnDevice.connIndex = newConnIndex;
|
||||
// Use the new conn to send the notification
|
||||
newConn.pushNotification(notification, apnDevice);
|
||||
}
|
||||
|
||||
function chooseConns(conns, device) {
|
||||
// If device does not have appIdentifier, all conns maybe proper connections.
|
||||
// Otherwise we try to match the appIdentifier with bundleId
|
||||
let qualifiedConns = [];
|
||||
for (let index = 0; index < conns.length; index++) {
|
||||
let conn = conns[index];
|
||||
// If the device we need to send to does not have
|
||||
// appIdentifier, any conn could be a qualified connection
|
||||
if (!device.appIdentifier || device.appIdentifier === '') {
|
||||
qualifiedConns.push(index);
|
||||
continue;
|
||||
}
|
||||
if (device.appIdentifier === conn.bundleId) {
|
||||
qualifiedConns.push(index);
|
||||
}
|
||||
}
|
||||
return qualifiedConns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the apns notification from the data we get from api request.
|
||||
* @param {Object} coreData The data field under api request body
|
||||
* @returns {Object} A apns notification
|
||||
*/
|
||||
let generateNotification = function(coreData, expirationTime) {
|
||||
function generateNotification(coreData, expirationTime) {
|
||||
let notification = new apn.notification();
|
||||
let payload = {};
|
||||
for (let key in coreData) {
|
||||
@@ -101,5 +202,7 @@ let generateNotification = function(coreData, expirationTime) {
|
||||
|
||||
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||
APNS.generateNotification = generateNotification;
|
||||
APNS.chooseConns = chooseConns;
|
||||
APNS.handleTransmissionError = handleTransmissionError;
|
||||
}
|
||||
module.exports = APNS;
|
||||
|
||||
@@ -9,12 +9,7 @@ const APNS = require('../../APNS');
|
||||
|
||||
function ParsePushAdapter(pushConfig) {
|
||||
this.validPushTypes = ['ios', 'android'];
|
||||
this.senders = {};
|
||||
|
||||
// Initialize senders
|
||||
for (let validPushType of this.validPushTypes) {
|
||||
this.senders[validPushType] = [];
|
||||
}
|
||||
this.senderMap = {};
|
||||
|
||||
pushConfig = pushConfig || {};
|
||||
let pushTypes = Object.keys(pushConfig);
|
||||
@@ -23,47 +18,17 @@ function ParsePushAdapter(pushConfig) {
|
||||
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);
|
||||
switch (pushType) {
|
||||
case 'ios':
|
||||
this.senderMap[pushType] = new APNS(pushConfig[pushType]);
|
||||
break;
|
||||
case 'android':
|
||||
this.senderMap[pushType] = new GCM(pushConfig[pushType]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -76,24 +41,18 @@ 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));
|
||||
}
|
||||
let sender = this.senderMap[pushType];
|
||||
if (!sender) {
|
||||
console.log('Can not find sender for push type %s, %j', pushType, data);
|
||||
continue;
|
||||
}
|
||||
let devices = deviceMap[pushType];
|
||||
sendPromises.push(sender.send(data, devices));
|
||||
}
|
||||
return Parse.Promise.when(sendPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
/**g
|
||||
* 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)
|
||||
@@ -113,7 +72,8 @@ function classifyInstallation(installations, validPushTypes) {
|
||||
let pushType = installation.deviceType;
|
||||
if (deviceMap[pushType]) {
|
||||
deviceMap[pushType].push({
|
||||
deviceToken: installation.deviceToken
|
||||
deviceToken: installation.deviceToken,
|
||||
appIdentifier: installation.appIdentifier
|
||||
});
|
||||
} else {
|
||||
console.log('Unknown push type from installation %j', installation);
|
||||
@@ -122,26 +82,7 @@ function classifyInstallation(installations, validPushTypes) {
|
||||
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;
|
||||
|
||||
62
src/GCM.js
62
src/GCM.js
@@ -8,6 +8,10 @@ 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);
|
||||
}
|
||||
|
||||
@@ -18,10 +22,6 @@ function GCM(args) {
|
||||
* @returns {Object} A promise which is resolved after we get results from gcm
|
||||
*/
|
||||
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.');
|
||||
}
|
||||
let pushId = randomstring.generate({
|
||||
length: 10,
|
||||
charset: 'alphanumeric'
|
||||
@@ -37,21 +37,30 @@ GCM.prototype.send = function(data, devices) {
|
||||
let gcmPayload = generateGCMPayload(data.data, pushId, timeStamp, expirationTime);
|
||||
// Make and send gcm request
|
||||
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
|
||||
|
||||
let sendPromises = [];
|
||||
// For android, we can only have 1000 recepients per send, so we need to slice devices to
|
||||
// chunk if necessary
|
||||
let chunkDevices = sliceDevices(devices, GCMRegistrationTokensMax);
|
||||
for (let chunkDevice of chunkDevices) {
|
||||
let sendPromise = new Parse.Promise();
|
||||
let registrationTokens = []
|
||||
for (let device of chunkDevice) {
|
||||
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
|
||||
});
|
||||
sendPromise.resolve();
|
||||
});
|
||||
promise.resolve();
|
||||
});
|
||||
return promise;
|
||||
sendPromises.push(sendPromise);
|
||||
}
|
||||
|
||||
return Parse.Promise.when(sendPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,7 +71,7 @@ GCM.prototype.send = function(data, devices) {
|
||||
* @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
|
||||
*/
|
||||
let generateGCMPayload = function(coreData, pushId, timeStamp, expirationTime) {
|
||||
function generateGCMPayload(coreData, pushId, timeStamp, expirationTime) {
|
||||
let payloadData = {
|
||||
'time': new Date(timeStamp).toISOString(),
|
||||
'push_id': pushId,
|
||||
@@ -86,9 +95,22 @@ let generateGCMPayload = function(coreData, pushId, timeStamp, expirationTime) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
GCM.GCMRegistrationTokensMax = GCMRegistrationTokensMax;
|
||||
/**
|
||||
* 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;
|
||||
|
||||
Reference in New Issue
Block a user