Upgrade APNS to use HTTP/2
- uses universal certificate - removes tests logs - standardized returned promises from APNS and GCM to something usable in _PushStatus
This commit is contained in:
@@ -18,7 +18,6 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"apn": "^1.7.5",
|
||||
"aws-sdk": "~2.2.33",
|
||||
"babel-polyfill": "^6.5.0",
|
||||
"babel-runtime": "^6.5.0",
|
||||
@@ -29,6 +28,7 @@
|
||||
"deepcopy": "^0.6.1",
|
||||
"express": "^4.13.4",
|
||||
"gcloud": "^0.28.0",
|
||||
"http2": "^3.3.2",
|
||||
"mailgun-js": "^0.7.7",
|
||||
"mime": "^1.3.4",
|
||||
"mongodb": "~2.1.0",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use strict';
|
||||
var APNS = require('../src/APNS');
|
||||
|
||||
describe('APNS', () => {
|
||||
@@ -9,17 +10,13 @@ describe('APNS', () => {
|
||||
production: true,
|
||||
bundleId: 'bundleId'
|
||||
}
|
||||
var apns = new APNS(args);
|
||||
var apns = 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);
|
||||
var apnsConfiguration = apns.getConfiguration();
|
||||
expect(apnsConfiguration.bundleId).toBe(args.bundleId);
|
||||
expect(apnsConfiguration.cert).toBe(args.cert);
|
||||
expect(apnsConfiguration.key).toBe(args.key);
|
||||
expect(apnsConfiguration.production).toBe(args.production);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -39,24 +36,18 @@ describe('APNS', () => {
|
||||
}
|
||||
]
|
||||
|
||||
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 apns = APNS(args);
|
||||
var devApnsConfiguration = apns.getConfiguration('bundleId');
|
||||
expect(devApnsConfiguration.cert).toBe(args[0].cert);
|
||||
expect(devApnsConfiguration.key).toBe(args[0].key);
|
||||
expect(devApnsConfiguration.production).toBe(args[0].production);
|
||||
expect(devApnsConfiguration.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);
|
||||
var prodApnsConfiguration = apns.getConfiguration('bundleIdAgain');
|
||||
expect(prodApnsConfiguration.cert).toBe(args[1].cert);
|
||||
expect(prodApnsConfiguration.key).toBe(args[1].key);
|
||||
expect(prodApnsConfiguration.production).toBe(args[1].production);
|
||||
expect(prodApnsConfiguration.bundleId).toBe(args[1].bundleId);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -73,56 +64,14 @@ describe('APNS', () => {
|
||||
};
|
||||
var expirationTime = 1454571491354
|
||||
|
||||
var notification = APNS.generateNotification(data, expirationTime);
|
||||
|
||||
expect(notification.alert).toEqual(data.alert);
|
||||
expect(notification.badge).toEqual(data.badge);
|
||||
expect(notification.sound).toEqual(data.sound);
|
||||
expect(notification.contentAvailable).toEqual(1);
|
||||
expect(notification.category).toEqual(data.category);
|
||||
expect(notification.payload).toEqual({
|
||||
'key': 'value',
|
||||
'keyAgain': 'valueAgain'
|
||||
});
|
||||
expect(notification.expiry).toEqual(expirationTime);
|
||||
done();
|
||||
});
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
var qualifiedConns = APNS.chooseConns(conns, device);
|
||||
expect(qualifiedConns).toEqual([0]);
|
||||
var notification = APNS.generateNotification(data);
|
||||
expect(notification.aps.alert).toEqual(data.alert);
|
||||
expect(notification.aps.badge).toEqual(data.badge);
|
||||
expect(notification.aps.sound).toEqual(data.sound);
|
||||
expect(notification.aps['content-available']).toEqual(1);
|
||||
expect(notification.aps.category).toEqual(data.category);
|
||||
expect(notification.key).toEqual('value');
|
||||
expect(notification.keyAgain).toEqual('valueAgain');
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -130,7 +79,7 @@ describe('APNS', () => {
|
||||
// Mock conns
|
||||
var conns = [
|
||||
{
|
||||
bundleId: 'bundleId'
|
||||
bundleId: 'bundleId',
|
||||
},
|
||||
{
|
||||
bundleId: 'bundleIdAgain'
|
||||
@@ -140,143 +89,18 @@ describe('APNS', () => {
|
||||
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();
|
||||
let apns = APNS(conns);
|
||||
var config = apns.getConfiguration(device.appIdentifier);
|
||||
expect(config).toBeUndefined();
|
||||
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 ];
|
||||
var apns = APNS(args);
|
||||
// Mock data
|
||||
var expirationTime = 1454571491354
|
||||
var data = {
|
||||
@@ -293,15 +117,18 @@ describe('APNS', () => {
|
||||
}
|
||||
];
|
||||
|
||||
var promise = apns.send(data, devices);
|
||||
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']);
|
||||
var apnDevice = args[1]
|
||||
expect(apnDevice.connIndex).toEqual(0);
|
||||
expect(apnDevice.appIdentifier).toEqual('bundleId');
|
||||
done();
|
||||
apns.send(data, devices).then((results) => {
|
||||
let isArray = Array.isArray(results);
|
||||
expect(isArray).toBe(true);
|
||||
expect(results.length).toBe(1);
|
||||
// No provided certificates
|
||||
expect(results[0].status).toBe(403);
|
||||
expect(results[0].device).toEqual(devices[0]);
|
||||
expect(results[0].transmitted).toBe(false);
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('should not fail');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,17 +23,15 @@ describe('GCM', () => {
|
||||
var data = {
|
||||
'alert': 'alert'
|
||||
};
|
||||
var pushId = 1;
|
||||
var timeStamp = 1454538822113;
|
||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
||||
|
||||
var payload = GCM.generateGCMPayload(data, pushId, timeStamp);
|
||||
var payload = GCM.generateGCMPayload(data, timeStamp);
|
||||
|
||||
expect(payload.priority).toEqual('normal');
|
||||
expect(payload.timeToLive).toEqual(undefined);
|
||||
var dataFromPayload = payload.data;
|
||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||
expect(dataFromUser).toEqual(data);
|
||||
done();
|
||||
@@ -44,18 +42,16 @@ describe('GCM', () => {
|
||||
var data = {
|
||||
'alert': 'alert'
|
||||
};
|
||||
var pushId = 1;
|
||||
var timeStamp = 1454538822113;
|
||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
||||
var expirationTime = 1454538922113
|
||||
|
||||
var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
|
||||
var payload = GCM.generateGCMPayload(data, timeStamp, expirationTime);
|
||||
|
||||
expect(payload.priority).toEqual('normal');
|
||||
expect(payload.timeToLive).toEqual(Math.floor((expirationTime - timeStamp) / 1000));
|
||||
var dataFromPayload = payload.data;
|
||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||
expect(dataFromUser).toEqual(data);
|
||||
done();
|
||||
@@ -66,18 +62,16 @@ describe('GCM', () => {
|
||||
var data = {
|
||||
'alert': 'alert'
|
||||
};
|
||||
var pushId = 1;
|
||||
var timeStamp = 1454538822113;
|
||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
||||
var expirationTime = 1454538822112;
|
||||
|
||||
var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
|
||||
var payload = GCM.generateGCMPayload(data, timeStamp, expirationTime);
|
||||
|
||||
expect(payload.priority).toEqual('normal');
|
||||
expect(payload.timeToLive).toEqual(0);
|
||||
var dataFromPayload = payload.data;
|
||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||
expect(dataFromUser).toEqual(data);
|
||||
done();
|
||||
@@ -88,19 +82,17 @@ describe('GCM', () => {
|
||||
var data = {
|
||||
'alert': 'alert'
|
||||
};
|
||||
var pushId = 1;
|
||||
var timeStamp = 1454538822113;
|
||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
||||
var expirationTime = 2454538822113;
|
||||
|
||||
var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
|
||||
var payload = GCM.generateGCMPayload(data, timeStamp, expirationTime);
|
||||
|
||||
expect(payload.priority).toEqual('normal');
|
||||
// Four week in second
|
||||
expect(payload.timeToLive).toEqual(4 * 7 * 24 * 60 * 60);
|
||||
var dataFromPayload = payload.data;
|
||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||
expect(dataFromUser).toEqual(data);
|
||||
done();
|
||||
@@ -139,6 +131,46 @@ describe('GCM', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can send GCM request', (done) => {
|
||||
var gcm = new GCM({
|
||||
apiKey: 'apiKey'
|
||||
});
|
||||
// Mock data
|
||||
var expirationTime = 2454538822113;
|
||||
var data = {
|
||||
'expiration_time': expirationTime,
|
||||
'data': {
|
||||
'alert': 'alert'
|
||||
}
|
||||
}
|
||||
// Mock devices
|
||||
var devices = [
|
||||
{
|
||||
deviceToken: 'token'
|
||||
},
|
||||
{
|
||||
deviceToken: 'token2'
|
||||
},
|
||||
{
|
||||
deviceToken: 'token3'
|
||||
},
|
||||
{
|
||||
deviceToken: 'token4'
|
||||
}
|
||||
];
|
||||
|
||||
gcm.send(data, devices).then((response) => {
|
||||
expect(Array.isArray(response)).toBe(true);
|
||||
expect(response.length).toEqual(devices.length);
|
||||
expect(response.length).toEqual(4);
|
||||
response.forEach((res, index) => {
|
||||
expect(res.transmitted).toEqual(false);
|
||||
expect(res.device).toEqual(devices[index]);
|
||||
})
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('can slice devices', (done) => {
|
||||
// Mock devices
|
||||
var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)];
|
||||
|
||||
@@ -3,18 +3,20 @@ describe('Parse.Push', () => {
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.data.badge;
|
||||
installations.forEach((installation) => {
|
||||
let promises = installations.map((installation) => {
|
||||
if (installation.deviceType == "ios") {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(installation.originalBadge+1).toEqual(installation.badge);
|
||||
} else {
|
||||
expect(installation.badge).toBeUndefined();
|
||||
}
|
||||
return Promise.resolve({
|
||||
err: null,
|
||||
deviceType: installation.deviceType,
|
||||
result: true
|
||||
})
|
||||
});
|
||||
return Promise.resolve({
|
||||
body: body,
|
||||
installations: installations
|
||||
});
|
||||
return Promise.all(promises)
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios", "android"];
|
||||
@@ -56,4 +58,4 @@ describe('Parse.Push', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1153,7 +1153,6 @@ describe('Parse.ACL', () => {
|
||||
var query = new Parse.Query("TestClassMasterACL");
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
console.log(JSON.stringify(results[0]));
|
||||
ok(!results.length, 'Should not have returned object with secure ACL.');
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -29,7 +29,9 @@ describe('ParsePushAdapter', () => {
|
||||
var parsePushAdapter = new ParsePushAdapter(pushConfig);
|
||||
// Check ios
|
||||
var iosSender = parsePushAdapter.senderMap['ios'];
|
||||
expect(iosSender instanceof APNS).toBe(true);
|
||||
expect(iosSender).not.toBe(undefined);
|
||||
expect(typeof iosSender.send).toEqual('function');
|
||||
expect(typeof iosSender.getConfiguration).toEqual('function');
|
||||
// Check android
|
||||
var androidSender = parsePushAdapter.senderMap['android'];
|
||||
expect(androidSender instanceof GCM).toBe(true);
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('PushController', () => {
|
||||
}
|
||||
})
|
||||
return Promise.resolve({
|
||||
error: null
|
||||
error: null,
|
||||
payload: body,
|
||||
})
|
||||
},
|
||||
|
||||
297
src/APNS.js
297
src/APNS.js
@@ -1,9 +1,30 @@
|
||||
"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.
|
||||
const apn = require('apn');
|
||||
const http = require('http2');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const urlParse = require('url').parse;
|
||||
|
||||
const DEV_PUSH_SERVER = 'api.development.push.apple.com';
|
||||
const PROD_PUSH_SERVER = 'api.push.apple.com';
|
||||
|
||||
const createRequestOptions = (opts, device, body) => {
|
||||
let domain = opts.production === true ? PROD_PUSH_SERVER : DEV_PUSH_SERVER;
|
||||
var options = urlParse(`https://${domain}/3/device/${device.deviceToken}`);
|
||||
options.method = 'POST';
|
||||
options.headers = {
|
||||
'apns-expiration': opts.expiration || 0,
|
||||
'apns-priority': opts.priority || 10,
|
||||
'apns-topic': opts.bundleId || opts['apns-topic'],
|
||||
'content-length': body.length
|
||||
};
|
||||
options.key = opts.key;
|
||||
options.cert = opts.cert;
|
||||
options.pfx = opts.pfx;
|
||||
options.passphrase = opts.passphrase;
|
||||
return Object.assign({}, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new connection to the APN service.
|
||||
@@ -16,175 +37,115 @@ const apn = require('apn');
|
||||
* @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) {
|
||||
// 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');
|
||||
function APNS(options) {
|
||||
|
||||
if (!Array.isArray(options)) {
|
||||
options = [options];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
let agents = {};
|
||||
|
||||
// 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) {
|
||||
if (device.callback) {
|
||||
device.callback(null, {
|
||||
notification: notification,
|
||||
device: device
|
||||
});
|
||||
let optionsByBundle = options.reduce((memo, option) => {
|
||||
try {
|
||||
if (option.key && option.cert) {
|
||||
option.key = fs.readFileSync(option.key);
|
||||
option.cert = fs.readFileSync(option.cert);
|
||||
} else if (option.pfx) {
|
||||
option.pfx = fs.readFileSync(option.pfx);
|
||||
} else {
|
||||
throw 'Either cert AND key, OR pfx is required'
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send apns request.
|
||||
* @param {Object} data The data we need to send, the format is the same with api request body
|
||||
* @param {Array} devices A array of devices
|
||||
* @returns {Object} A promise which is resolved immediately
|
||||
*/
|
||||
APNS.prototype.send = function(data, devices) {
|
||||
let coreData = data.data;
|
||||
let expirationTime = data['expiration_time'];
|
||||
let notification = generateNotification(coreData, expirationTime);
|
||||
|
||||
let promises = devices.map((device) => {
|
||||
let qualifiedConnIndexs = chooseConns(this.conns, device);
|
||||
// We can not find a valid conn, just ignore this device
|
||||
if (qualifiedConnIndexs.length == 0) {
|
||||
return Promise.resolve({
|
||||
err: 'No connection available'
|
||||
});
|
||||
}
|
||||
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;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
apnDevice.callback = (err, res) => {
|
||||
resolve({
|
||||
error: err,
|
||||
response: res,
|
||||
payload: notification,
|
||||
deviceType: 'ios'
|
||||
});
|
||||
} catch(e) {
|
||||
if (!process.env.NODE_ENV == 'test' || options.enforceCertificates) {
|
||||
throw e;
|
||||
}
|
||||
conn.pushNotification(notification, apnDevice);
|
||||
}
|
||||
option.agent = new http.Agent({
|
||||
key: option.key,
|
||||
cert: option.cert,
|
||||
pfx: option.pfx,
|
||||
passphrase: option.passphrase
|
||||
});
|
||||
});
|
||||
return Parse.Promise.when(promises);
|
||||
}
|
||||
memo[option.bundleId] = option;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
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;
|
||||
let getConfiguration = (bundleIdentifier) => {
|
||||
let configuration;
|
||||
if (bundleIdentifier) {
|
||||
configuration = optionsByBundle[bundleIdentifier];
|
||||
if (!configuration) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!configuration) {
|
||||
configuration = options[0];
|
||||
}
|
||||
return configuration;
|
||||
}
|
||||
// There is no more available conns, we give up in this case
|
||||
if (newConnIndex < 0 || newConnIndex >= conns.length) {
|
||||
if (apnDevice.callback) {
|
||||
apnDevice.callback({
|
||||
error: `APNS can not find vaild connection for ${apnDevice.token}`,
|
||||
code: errCode
|
||||
|
||||
/**
|
||||
* Send apns request.
|
||||
* @param {Object} data The data we need to send, the format is the same with api request body
|
||||
* @param {Array} devices A array of device tokens
|
||||
* @returns {Object} A promises that resolves with each notificaiton sending promise
|
||||
*/
|
||||
let send = function(data, devices) {
|
||||
// Make sure devices are in an array
|
||||
if (!Array.isArray(devices)) {
|
||||
devices = [devices];
|
||||
}
|
||||
|
||||
let coreData = data.data;
|
||||
let expirationTime = data['expiration_time'];
|
||||
let notification = generateNotification(coreData);
|
||||
let notificationString = JSON.stringify(notification);
|
||||
let buffer = new Buffer(notificationString);
|
||||
|
||||
let promises = devices.map((device) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let configuration = getConfiguration(device.appIdentifier);
|
||||
if (!configuration) {
|
||||
return Promise.reject({
|
||||
status: -1,
|
||||
device: device,
|
||||
response: {"error": "No configuration set for that appIdentifier"},
|
||||
transmitted: false
|
||||
})
|
||||
}
|
||||
configuration = Object.assign({}, configuration, {expiration: expirationTime })
|
||||
let requestOptions = createRequestOptions(configuration, device, buffer);
|
||||
let req = configuration.agent.request(requestOptions, (response) => {
|
||||
response.setEncoding('utf8');
|
||||
var chunks = "";
|
||||
response.on('data', (chunk) => {
|
||||
chunks+=chunk;
|
||||
});
|
||||
response.on('end', () => {
|
||||
let body;
|
||||
try{
|
||||
body = JSON.parse(chunks);
|
||||
} catch (e) {
|
||||
body = {};
|
||||
}
|
||||
resolve({ status: response.statusCode,
|
||||
response: body,
|
||||
headers: response.headers,
|
||||
device: device,
|
||||
transmitted: response.statusCode == 200 });
|
||||
});
|
||||
});
|
||||
req.write(buffer);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
return;
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
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;
|
||||
return Object.freeze({
|
||||
send: send,
|
||||
getConfiguration: getConfiguration
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,12 +154,12 @@ function chooseConns(conns, device) {
|
||||
* @returns {Object} A apns notification
|
||||
*/
|
||||
function generateNotification(coreData, expirationTime) {
|
||||
let notification = new apn.notification();
|
||||
let payload = {};
|
||||
let notification = {};
|
||||
for (let key in coreData) {
|
||||
switch (key) {
|
||||
case 'alert':
|
||||
notification.setAlertText(coreData.alert);
|
||||
notification.alert = coreData.alert;
|
||||
break;
|
||||
case 'badge':
|
||||
notification.badge = coreData.badge;
|
||||
@@ -207,9 +168,10 @@ function generateNotification(coreData, expirationTime) {
|
||||
notification.sound = coreData.sound;
|
||||
break;
|
||||
case 'content-available':
|
||||
notification.setNewsstandAvailable(true);
|
||||
let isAvailable = coreData['content-available'] === 1;
|
||||
notification.setContentAvailable(isAvailable);
|
||||
if (isAvailable) {
|
||||
notification['content-available'] = 1;
|
||||
}
|
||||
break;
|
||||
case 'category':
|
||||
notification.category = coreData.category;
|
||||
@@ -219,14 +181,11 @@ function generateNotification(coreData, expirationTime) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
notification.payload = payload;
|
||||
notification.expiry = expirationTime;
|
||||
return notification;
|
||||
payload.aps = notification;
|
||||
return payload;
|
||||
}
|
||||
|
||||
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||
APNS.generateNotification = generateNotification;
|
||||
APNS.chooseConns = chooseConns;
|
||||
APNS.handleTransmissionError = handleTransmissionError;
|
||||
}
|
||||
module.exports = APNS;
|
||||
|
||||
@@ -10,11 +10,11 @@ var deepcopy = require('deepcopy');
|
||||
import PushAdapter from './PushAdapter';
|
||||
|
||||
export class OneSignalPushAdapter extends PushAdapter {
|
||||
|
||||
|
||||
constructor(pushConfig = {}) {
|
||||
super(pushConfig);
|
||||
this.https = require('https');
|
||||
|
||||
|
||||
this.validPushTypes = ['ios', 'android'];
|
||||
this.senderMap = {};
|
||||
this.OneSignalConfig = {};
|
||||
@@ -24,13 +24,12 @@ export class OneSignalPushAdapter extends PushAdapter {
|
||||
}
|
||||
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
|
||||
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
|
||||
|
||||
|
||||
this.senderMap['ios'] = this.sendToAPNS.bind(this);
|
||||
this.senderMap['android'] = this.sendToGCM.bind(this);
|
||||
}
|
||||
|
||||
|
||||
send(data, installations) {
|
||||
console.log("Sending notification to "+installations.length+" devices.")
|
||||
let deviceMap = classifyInstallations(installations, this.validPushTypes);
|
||||
|
||||
let sendPromises = [];
|
||||
@@ -48,15 +47,15 @@ export class OneSignalPushAdapter extends PushAdapter {
|
||||
}
|
||||
return Parse.Promise.when(sendPromises);
|
||||
}
|
||||
|
||||
|
||||
static classifyInstallations(installations, validTypes) {
|
||||
return classifyInstallations(installations, validTypes)
|
||||
}
|
||||
|
||||
|
||||
getValidPushTypes() {
|
||||
return this.validPushTypes;
|
||||
}
|
||||
|
||||
|
||||
sendToAPNS(data,tokens) {
|
||||
|
||||
data= deepcopy(data['data']);
|
||||
@@ -117,19 +116,19 @@ export class OneSignalPushAdapter extends PushAdapter {
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
|
||||
sendToGCM(data,tokens) {
|
||||
data= deepcopy(data['data']);
|
||||
|
||||
var post = {};
|
||||
|
||||
|
||||
if(data['alert']) {
|
||||
post['contents'] = {en: data['alert']};
|
||||
delete data['alert'];
|
||||
}
|
||||
if(data['title']) {
|
||||
post['title'] = {en: data['title']};
|
||||
delete data['title'];
|
||||
delete data['title'];
|
||||
}
|
||||
if(data['uri']) {
|
||||
post['url'] = data['uri'];
|
||||
@@ -155,7 +154,7 @@ export class OneSignalPushAdapter extends PushAdapter {
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
this.sendNext = function() {
|
||||
this.sendNext = function() {
|
||||
post['include_android_reg_ids'] = [];
|
||||
tokens.slice(offset,offset+chunk).forEach(function(i) {
|
||||
post['include_android_reg_ids'].push(i['deviceToken'])
|
||||
@@ -168,7 +167,7 @@ export class OneSignalPushAdapter extends PushAdapter {
|
||||
this.sendNext();
|
||||
return promise;
|
||||
}
|
||||
|
||||
|
||||
sendToOneSignal(data, cb) {
|
||||
let headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -188,7 +187,7 @@ export class OneSignalPushAdapter extends PushAdapter {
|
||||
cb(true);
|
||||
} else {
|
||||
console.log('OneSignal Error');
|
||||
res.on('data', function(chunk) {
|
||||
res.on('data', function(chunk) {
|
||||
console.log(chunk.toString())
|
||||
});
|
||||
cb(false)
|
||||
|
||||
@@ -50,11 +50,14 @@ export class ParsePushAdapter extends PushAdapter {
|
||||
for (let pushType in deviceMap) {
|
||||
let sender = this.senderMap[pushType];
|
||||
if (!sender) {
|
||||
console.log('Can not find sender for push type %s, %j', pushType, data);
|
||||
continue;
|
||||
sendPromises.push(Promise.resolve({
|
||||
transmitted: false,
|
||||
response: {'error': `Can not find sender for push type ${pushType}, ${data}`}
|
||||
}))
|
||||
} else {
|
||||
let devices = deviceMap[pushType];
|
||||
sendPromises.push(sender.send(data, devices));
|
||||
}
|
||||
let devices = deviceMap[pushType];
|
||||
sendPromises.push(sender.send(data, devices));
|
||||
}
|
||||
return Parse.Promise.when(sendPromises);
|
||||
}
|
||||
|
||||
@@ -21,10 +21,7 @@ export function classifyInstallations(installations, validPushTypes) {
|
||||
deviceToken: installation.deviceToken,
|
||||
appIdentifier: installation.appIdentifier
|
||||
});
|
||||
} else {
|
||||
console.log('Unknown push type from installation %j', installation);
|
||||
}
|
||||
}
|
||||
return deviceMap;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ export class PushController extends AdaptableController {
|
||||
}
|
||||
return pushAdapter.send(body, response.results, pushStatus);
|
||||
}).then((results) => {
|
||||
console.log(results);
|
||||
// TODO: handle push results
|
||||
return Promise.resolve(results);
|
||||
});
|
||||
}
|
||||
|
||||
99
src/GCM.js
99
src/GCM.js
@@ -21,9 +21,30 @@ function GCM(args) {
|
||||
* @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, devices, callback) {
|
||||
let pushId = cryptoUtils.newObjectId();
|
||||
let timeStamp = Date.now();
|
||||
GCM.prototype.send = function(data, devices) {
|
||||
// Make a new array
|
||||
devices = new Array(...devices);
|
||||
let timestamp = Date.now();
|
||||
// For android, we can only have 1000 recepients per send, so we need to slice devices to
|
||||
// chunk if necessary
|
||||
let slices = sliceDevices(devices, GCMRegistrationTokensMax);
|
||||
if (slices.length > 1) {
|
||||
// Make 1 send per slice
|
||||
let promises = slices.reduce((memo, slice) => {
|
||||
let promise = this.send(data, slice, timestamp);
|
||||
memo.push(promise);
|
||||
return memo;
|
||||
}, [])
|
||||
return Parse.Promise.when(promises).then((results) => {
|
||||
let allResults = results.reduce((memo, result) => {
|
||||
return memo.concat(result);
|
||||
}, []);
|
||||
return Parse.Promise.as(allResults);
|
||||
});
|
||||
}
|
||||
// get the devices back...
|
||||
devices = slices[0];
|
||||
|
||||
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
|
||||
@@ -31,38 +52,51 @@ GCM.prototype.send = function(data, devices, callback) {
|
||||
expirationTime = data['expiration_time'];
|
||||
}
|
||||
// Generate gcm payload
|
||||
let gcmPayload = generateGCMPayload(data.data, pushId, timeStamp, expirationTime);
|
||||
let gcmPayload = generateGCMPayload(data.data, timestamp, expirationTime);
|
||||
// Make and send gcm request
|
||||
let message = new gcm.Message(gcmPayload);
|
||||
|
||||
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({
|
||||
error: error,
|
||||
response: response,
|
||||
payload: message,
|
||||
deviceType: 'android'
|
||||
});
|
||||
});
|
||||
sendPromises.push(sendPromise);
|
||||
}
|
||||
// Build a device map
|
||||
let devicesMap = devices.reduce((memo, device) => {
|
||||
memo[device.deviceToken] = device;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
return Parse.Promise.when(sendPromises);
|
||||
let deviceTokens = Object.keys(devicesMap);
|
||||
|
||||
let promises = deviceTokens.map(() => new Parse.Promise());
|
||||
let registrationTokens = deviceTokens;
|
||||
this.sender.send(message, { registrationTokens: registrationTokens }, 5, (error, response) => {
|
||||
// example response:
|
||||
/*
|
||||
{ "multicast_id":7680139367771848000,
|
||||
"success":0,
|
||||
"failure":4,
|
||||
"canonical_ids":0,
|
||||
"results":[ {"error":"InvalidRegistration"},
|
||||
{"error":"InvalidRegistration"},
|
||||
{"error":"InvalidRegistration"},
|
||||
{"error":"InvalidRegistration"}] }
|
||||
*/
|
||||
let { results, multicast_id } = response || {};
|
||||
registrationTokens.forEach((token, index) => {
|
||||
let promise = promises[index];
|
||||
let result = results ? results[index] : undefined;
|
||||
let device = devicesMap[token];
|
||||
let resolution = {
|
||||
device,
|
||||
multicast_id,
|
||||
response: error || result,
|
||||
};
|
||||
if (!result || result.error) {
|
||||
resolution.transmitted = false;
|
||||
} else {
|
||||
resolution.transmitted = true;
|
||||
}
|
||||
promise.resolve(resolution);
|
||||
});
|
||||
});
|
||||
return Parse.Promise.when(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,10 +107,9 @@ GCM.prototype.send = function(data, devices, callback) {
|
||||
* @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
|
||||
*/
|
||||
function generateGCMPayload(coreData, pushId, timeStamp, expirationTime) {
|
||||
function generateGCMPayload(coreData, timeStamp, expirationTime) {
|
||||
let payloadData = {
|
||||
'time': new Date(timeStamp).toISOString(),
|
||||
'push_id': pushId,
|
||||
'data': JSON.stringify(coreData)
|
||||
}
|
||||
let payload = {
|
||||
|
||||
Reference in New Issue
Block a user