Merge pull request #311 from ParsePlatform/wangmengyan.add_push_api
Add support for push
This commit is contained in:
@@ -1,6 +1,65 @@
|
|||||||
var APNS = require('../src/APNS');
|
var APNS = require('../src/APNS');
|
||||||
|
|
||||||
describe('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) => {
|
it('can generate APNS notification', (done) => {
|
||||||
//Mock request data
|
//Mock request data
|
||||||
var data = {
|
var data = {
|
||||||
@@ -29,12 +88,195 @@ describe('APNS', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can send APNS notification', (done) => {
|
it('can choose conns for device without appIdentifier', (done) => {
|
||||||
var apns = new APNS();
|
// Mock conns
|
||||||
var sender = {
|
var conns = [
|
||||||
pushNotification: jasmine.createSpy('send')
|
{
|
||||||
|
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
|
// Mock data
|
||||||
var expirationTime = 1454571491354
|
var expirationTime = 1454571491354
|
||||||
var data = {
|
var data = {
|
||||||
@@ -43,16 +285,23 @@ describe('APNS', () => {
|
|||||||
'alert': 'alert'
|
'alert': 'alert'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Mock registrationTokens
|
// Mock devices
|
||||||
var deviceTokens = ['token'];
|
var devices = [
|
||||||
|
{
|
||||||
|
deviceToken: '112233',
|
||||||
|
appIdentifier: 'bundleId'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
var promise = apns.send(data, deviceTokens);
|
var promise = apns.send(data, devices);
|
||||||
expect(sender.pushNotification).toHaveBeenCalled();
|
expect(conn.pushNotification).toHaveBeenCalled();
|
||||||
var args = sender.pushNotification.calls.first().args;
|
var args = conn.pushNotification.calls.first().args;
|
||||||
var notification = args[0];
|
var notification = args[0];
|
||||||
expect(notification.alert).toEqual(data.data.alert);
|
expect(notification.alert).toEqual(data.data.alert);
|
||||||
expect(notification.expiry).toEqual(data['expiration_time']);
|
expect(notification.expiry).toEqual(data['expiration_time']);
|
||||||
expect(args[1]).toEqual(deviceTokens);
|
var apnDevice = args[1]
|
||||||
|
expect(apnDevice.connIndex).toEqual(0);
|
||||||
|
expect(apnDevice.appIdentifier).toEqual('bundleId');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
var GCM = require('../src/GCM');
|
var GCM = require('../src/GCM');
|
||||||
|
|
||||||
describe('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) => {
|
it('can generate GCM Payload without expiration time', (done) => {
|
||||||
//Mock request data
|
//Mock request data
|
||||||
var data = {
|
var data = {
|
||||||
@@ -90,7 +107,9 @@ describe('GCM', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can send GCM request', (done) => {
|
it('can send GCM request', (done) => {
|
||||||
var gcm = new GCM('apiKey');
|
var gcm = new GCM({
|
||||||
|
apiKey: 'apiKey'
|
||||||
|
});
|
||||||
// Mock gcm sender
|
// Mock gcm sender
|
||||||
var sender = {
|
var sender = {
|
||||||
send: jasmine.createSpy('send')
|
send: jasmine.createSpy('send')
|
||||||
@@ -104,34 +123,37 @@ describe('GCM', () => {
|
|||||||
'alert': 'alert'
|
'alert': 'alert'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Mock registrationTokens
|
// Mock devices
|
||||||
var registrationTokens = ['token'];
|
var devices = [
|
||||||
|
{
|
||||||
|
deviceToken: 'token'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
var promise = gcm.send(data, registrationTokens);
|
gcm.send(data, devices);
|
||||||
expect(sender.send).toHaveBeenCalled();
|
expect(sender.send).toHaveBeenCalled();
|
||||||
var args = sender.send.calls.first().args;
|
var args = sender.send.calls.first().args;
|
||||||
// It is too hard to verify message of gcm library, we just verify tokens and retry times
|
// It is too hard to verify message of gcm library, we just verify tokens and retry times
|
||||||
expect(args[1].registrationTokens).toEqual(registrationTokens);
|
expect(args[1].registrationTokens).toEqual(['token']);
|
||||||
expect(args[2]).toEqual(5);
|
expect(args[2]).toEqual(5);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can throw on sending when we have too many registration tokens', (done) => {
|
it('can slice devices', (done) => {
|
||||||
var gcm = new GCM('apiKey');
|
// Mock devices
|
||||||
// Mock gcm sender
|
var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)];
|
||||||
var sender = {
|
|
||||||
send: jasmine.createSpy('send')
|
|
||||||
};
|
|
||||||
gcm.sender = sender;
|
|
||||||
// Mock registrationTokens
|
|
||||||
var registrationTokens = [];
|
|
||||||
for (var i = 0; i <= 2000; i++) {
|
|
||||||
registrationTokens.push(i.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(function() {
|
var chunkDevices = GCM.sliceDevices(devices, 3);
|
||||||
gcm.send({}, registrationTokens);
|
expect(chunkDevices).toEqual([
|
||||||
}).toThrow();
|
[makeDevice(1), makeDevice(2), makeDevice(3)],
|
||||||
|
[makeDevice(4)]
|
||||||
|
]);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function makeDevice(deviceToken) {
|
||||||
|
return {
|
||||||
|
deviceToken: deviceToken
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
150
spec/ParsePushAdapter.spec.js
Normal file
150
spec/ParsePushAdapter.spec.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
var ParsePushAdapter = require('../src/Adapters/Push/ParsePushAdapter');
|
||||||
|
var APNS = require('../src/APNS');
|
||||||
|
var GCM = require('../src/GCM');
|
||||||
|
|
||||||
|
describe('ParsePushAdapter', () => {
|
||||||
|
it('can be initialized', (done) => {
|
||||||
|
// Make mock config
|
||||||
|
var pushConfig = {
|
||||||
|
android: {
|
||||||
|
senderId: 'senderId',
|
||||||
|
apiKey: 'apiKey'
|
||||||
|
},
|
||||||
|
ios: [
|
||||||
|
{
|
||||||
|
cert: 'prodCert.pem',
|
||||||
|
key: 'prodKey.pem',
|
||||||
|
production: true,
|
||||||
|
bundleId: 'bundleId'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cert: 'devCert.pem',
|
||||||
|
key: 'devKey.pem',
|
||||||
|
production: false,
|
||||||
|
bundleId: 'bundleIdAgain'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var parsePushAdapter = new ParsePushAdapter(pushConfig);
|
||||||
|
// Check ios
|
||||||
|
var iosSender = parsePushAdapter.senderMap['ios'];
|
||||||
|
expect(iosSender instanceof APNS).toBe(true);
|
||||||
|
// Check android
|
||||||
|
var androidSender = parsePushAdapter.senderMap['android'];
|
||||||
|
expect(androidSender instanceof GCM).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can throw on initializing with unsupported push type', (done) => {
|
||||||
|
// Make mock config
|
||||||
|
var pushConfig = {
|
||||||
|
win: {
|
||||||
|
senderId: 'senderId',
|
||||||
|
apiKey: 'apiKey'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(function() {
|
||||||
|
new ParsePushAdapter(pushConfig);
|
||||||
|
}).toThrow();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can get valid push types', (done) => {
|
||||||
|
var parsePushAdapter = new ParsePushAdapter();
|
||||||
|
|
||||||
|
expect(parsePushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can classify installation', (done) => {
|
||||||
|
// Mock installations
|
||||||
|
var validPushTypes = ['ios', 'android'];
|
||||||
|
var installations = [
|
||||||
|
{
|
||||||
|
deviceType: 'android',
|
||||||
|
deviceToken: 'androidToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'ios',
|
||||||
|
deviceToken: 'iosToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'win',
|
||||||
|
deviceToken: 'winToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'android',
|
||||||
|
deviceToken: undefined
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var deviceMap = ParsePushAdapter.classifyInstallation(installations, validPushTypes);
|
||||||
|
expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
|
||||||
|
expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
|
||||||
|
expect(deviceMap['win']).toBe(undefined);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('can send push notifications', (done) => {
|
||||||
|
var parsePushAdapter = new ParsePushAdapter();
|
||||||
|
// Mock android ios senders
|
||||||
|
var androidSender = {
|
||||||
|
send: jasmine.createSpy('send')
|
||||||
|
};
|
||||||
|
var iosSender = {
|
||||||
|
send: jasmine.createSpy('send')
|
||||||
|
};
|
||||||
|
var senderMap = {
|
||||||
|
ios: iosSender,
|
||||||
|
android: androidSender
|
||||||
|
};
|
||||||
|
parsePushAdapter.senderMap = senderMap;
|
||||||
|
// Mock installations
|
||||||
|
var installations = [
|
||||||
|
{
|
||||||
|
deviceType: 'android',
|
||||||
|
deviceToken: 'androidToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'ios',
|
||||||
|
deviceToken: 'iosToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'win',
|
||||||
|
deviceToken: 'winToken'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'android',
|
||||||
|
deviceToken: undefined
|
||||||
|
}
|
||||||
|
];
|
||||||
|
var data = {};
|
||||||
|
|
||||||
|
parsePushAdapter.send(data, installations);
|
||||||
|
// Check android sender
|
||||||
|
expect(androidSender.send).toHaveBeenCalled();
|
||||||
|
var args = androidSender.send.calls.first().args;
|
||||||
|
expect(args[0]).toEqual(data);
|
||||||
|
expect(args[1]).toEqual([
|
||||||
|
makeDevice('androidToken')
|
||||||
|
]);
|
||||||
|
// Check ios sender
|
||||||
|
expect(iosSender.send).toHaveBeenCalled();
|
||||||
|
args = iosSender.send.calls.first().args;
|
||||||
|
expect(args[0]).toEqual(data);
|
||||||
|
expect(args[1]).toEqual([
|
||||||
|
makeDevice('iosToken')
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeDevice(deviceToken, appIdentifier) {
|
||||||
|
return {
|
||||||
|
deviceToken: deviceToken,
|
||||||
|
appIdentifier: appIdentifier
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
var push = require('../src/push');
|
var PushController = require('../src/Controllers/PushController').PushController;
|
||||||
|
|
||||||
describe('push', () => {
|
describe('PushController', () => {
|
||||||
it('can check valid master key of request', (done) => {
|
it('can check valid master key of request', (done) => {
|
||||||
// Make mock request
|
// Make mock request
|
||||||
var request = {
|
var request = {
|
||||||
@@ -13,7 +13,7 @@ describe('push', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
push.validateMasterKey(request);
|
PushController.validateMasterKey(request);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -30,7 +30,7 @@ describe('push', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
push.validateMasterKey(request);
|
PushController.validateMasterKey(request);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -43,7 +43,7 @@ describe('push', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var where = push.getQueryCondition(request);
|
var where = PushController.getQueryCondition(request);
|
||||||
expect(where).toEqual({
|
expect(where).toEqual({
|
||||||
'channels': {
|
'channels': {
|
||||||
'$in': ['Giants', 'Mets']
|
'$in': ['Giants', 'Mets']
|
||||||
@@ -62,7 +62,7 @@ describe('push', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var where = push.getQueryCondition(request);
|
var where = PushController.getQueryCondition(request);
|
||||||
expect(where).toEqual({
|
expect(where).toEqual({
|
||||||
'injuryReports': true
|
'injuryReports': true
|
||||||
});
|
});
|
||||||
@@ -77,7 +77,7 @@ describe('push', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(function() {
|
expect(function() {
|
||||||
push.getQueryCondition(request);
|
PushController.getQueryCondition(request);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -96,7 +96,7 @@ describe('push', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(function() {
|
expect(function() {
|
||||||
push.getQueryCondition(request);
|
PushController.getQueryCondition(request);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -104,10 +104,11 @@ describe('push', () => {
|
|||||||
it('can validate device type when no device type is set', (done) => {
|
it('can validate device type when no device type is set', (done) => {
|
||||||
// Make query condition
|
// Make query condition
|
||||||
var where = {
|
var where = {
|
||||||
}
|
};
|
||||||
|
var validPushTypes = ['ios', 'android'];
|
||||||
|
|
||||||
expect(function(){
|
expect(function(){
|
||||||
push.validateDeviceType(where);
|
PushController.validatePushType(where, validPushTypes);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -116,10 +117,11 @@ describe('push', () => {
|
|||||||
// Make query condition
|
// Make query condition
|
||||||
var where = {
|
var where = {
|
||||||
'deviceType': 'ios'
|
'deviceType': 'ios'
|
||||||
}
|
};
|
||||||
|
var validPushTypes = ['ios', 'android'];
|
||||||
|
|
||||||
expect(function(){
|
expect(function(){
|
||||||
push.validateDeviceType(where);
|
PushController.validatePushType(where, validPushTypes);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -130,10 +132,11 @@ describe('push', () => {
|
|||||||
'deviceType': {
|
'deviceType': {
|
||||||
'$in': ['android', 'ios']
|
'$in': ['android', 'ios']
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
var validPushTypes = ['ios', 'android'];
|
||||||
|
|
||||||
expect(function(){
|
expect(function(){
|
||||||
push.validateDeviceType(where);
|
PushController.validatePushType(where, validPushTypes);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -142,10 +145,11 @@ describe('push', () => {
|
|||||||
// Make query condition
|
// Make query condition
|
||||||
var where = {
|
var where = {
|
||||||
'deviceType': 'osx'
|
'deviceType': 'osx'
|
||||||
}
|
};
|
||||||
|
var validPushTypes = ['ios', 'android'];
|
||||||
|
|
||||||
expect(function(){
|
expect(function(){
|
||||||
push.validateDeviceType(where);
|
PushController.validatePushType(where, validPushTypes);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -154,10 +158,11 @@ describe('push', () => {
|
|||||||
// Make query condition
|
// Make query condition
|
||||||
var where = {
|
var where = {
|
||||||
'deviceType': 'osx'
|
'deviceType': 'osx'
|
||||||
}
|
};
|
||||||
|
var validPushTypes = ['ios', 'android'];
|
||||||
|
|
||||||
expect(function(){
|
expect(function(){
|
||||||
push.validateDeviceType(where)
|
PushController.validatePushType(where, validPushTypes);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -171,7 +176,7 @@ describe('push', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var time = push.getExpirationTime(request);
|
var time = PushController.getExpirationTime(request);
|
||||||
expect(time).toEqual(new Date(timeStr).valueOf());
|
expect(time).toEqual(new Date(timeStr).valueOf());
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -185,7 +190,7 @@ describe('push', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var time = push.getExpirationTime(request);
|
var time = PushController.getExpirationTime(request);
|
||||||
expect(time).toEqual(timeNumber * 1000);
|
expect(time).toEqual(timeNumber * 1000);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -199,7 +204,7 @@ describe('push', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(function(){
|
expect(function(){
|
||||||
push.getExpirationTime(request);
|
PushController.getExpirationTime(request);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
187
src/APNS.js
187
src/APNS.js
@@ -1,66 +1,177 @@
|
|||||||
var Parse = require('parse/node').Parse;
|
"use strict";
|
||||||
|
|
||||||
|
const Parse = require('parse/node').Parse;
|
||||||
// TODO: apn does not support the new HTTP/2 protocal. It is fine to use it in V1,
|
// 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.
|
// but probably we will replace it in the future.
|
||||||
var apn = require('apn');
|
const apn = require('apn');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new connection to the APN service.
|
* Create a new connection to the APN service.
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {Object} args Arguments to config APNS connection
|
* @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, default is cert.pem
|
* @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, default is key.pem
|
* @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.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
|
* @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox
|
||||||
*/
|
*/
|
||||||
function APNS(args) {
|
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() {
|
this.conns = [];
|
||||||
console.log('APNS Connected');
|
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
|
||||||
this.sender.on('transmissionError', function(errCode, notification, device) {
|
for (let index = 0; index < this.conns.length; index++) {
|
||||||
console.error('APNS Notification caused error: ' + errCode + ' for device ', device, notification);
|
this.conns[index].index = index;
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send apns request.
|
* Send apns request.
|
||||||
* @param {Object} data The data we need to send, the format is the same with api request body
|
* @param {Object} data The data we need to send, the format is the same with api request body
|
||||||
* @param {Array} deviceTokens A array of device tokens
|
* @param {Array} devices A array of devices
|
||||||
* @returns {Object} A promise which is resolved immediately
|
* @returns {Object} A promise which is resolved immediately
|
||||||
*/
|
*/
|
||||||
APNS.prototype.send = function(data, deviceTokens) {
|
APNS.prototype.send = function(data, devices) {
|
||||||
var coreData = data.data;
|
let coreData = data.data;
|
||||||
var expirationTime = data['expiration_time'];
|
let expirationTime = data['expiration_time'];
|
||||||
var notification = generateNotification(coreData, expirationTime);
|
let notification = generateNotification(coreData, expirationTime);
|
||||||
this.sender.pushNotification(notification, deviceTokens);
|
for (let device of devices) {
|
||||||
// TODO: pushNotification will push the notification to apn's queue.
|
let qualifiedConnIndexs = chooseConns(this.conns, device);
|
||||||
// We do not handle error in V1, we just relies apn to auto retry and send the
|
// We can not find a valid conn, just ignore this device
|
||||||
// notifications.
|
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);
|
||||||
|
}
|
||||||
return Parse.Promise.as();
|
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.
|
* Generate the apns notification from the data we get from api request.
|
||||||
* @param {Object} coreData The data field under api request body
|
* @param {Object} coreData The data field under api request body
|
||||||
* @returns {Object} A apns notification
|
* @returns {Object} A apns notification
|
||||||
*/
|
*/
|
||||||
var generateNotification = function(coreData, expirationTime) {
|
function generateNotification(coreData, expirationTime) {
|
||||||
var notification = new apn.notification();
|
let notification = new apn.notification();
|
||||||
var payload = {};
|
let payload = {};
|
||||||
for (var key in coreData) {
|
for (let key in coreData) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'alert':
|
case 'alert':
|
||||||
notification.setAlertText(coreData.alert);
|
notification.setAlertText(coreData.alert);
|
||||||
@@ -73,7 +184,7 @@ var generateNotification = function(coreData, expirationTime) {
|
|||||||
break;
|
break;
|
||||||
case 'content-available':
|
case 'content-available':
|
||||||
notification.setNewsstandAvailable(true);
|
notification.setNewsstandAvailable(true);
|
||||||
var isAvailable = coreData['content-available'] === 1;
|
let isAvailable = coreData['content-available'] === 1;
|
||||||
notification.setContentAvailable(isAvailable);
|
notification.setContentAvailable(isAvailable);
|
||||||
break;
|
break;
|
||||||
case 'category':
|
case 'category':
|
||||||
@@ -91,5 +202,7 @@ var generateNotification = function(coreData, expirationTime) {
|
|||||||
|
|
||||||
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||||
APNS.generateNotification = generateNotification;
|
APNS.generateNotification = generateNotification;
|
||||||
|
APNS.chooseConns = chooseConns;
|
||||||
|
APNS.handleTransmissionError = handleTransmissionError;
|
||||||
}
|
}
|
||||||
module.exports = APNS;
|
module.exports = APNS;
|
||||||
|
|||||||
88
src/Adapters/Push/ParsePushAdapter.js
Normal file
88
src/Adapters/Push/ParsePushAdapter.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"use strict";
|
||||||
|
// ParsePushAdapter is the default implementation of
|
||||||
|
// PushAdapter, it uses GCM for android push and APNS
|
||||||
|
// for ios push.
|
||||||
|
|
||||||
|
const Parse = require('parse/node').Parse;
|
||||||
|
const GCM = require('../../GCM');
|
||||||
|
const APNS = require('../../APNS');
|
||||||
|
|
||||||
|
function ParsePushAdapter(pushConfig) {
|
||||||
|
this.validPushTypes = ['ios', 'android'];
|
||||||
|
this.senderMap = {};
|
||||||
|
|
||||||
|
pushConfig = pushConfig || {};
|
||||||
|
let pushTypes = Object.keys(pushConfig);
|
||||||
|
for (let pushType of pushTypes) {
|
||||||
|
if (this.validPushTypes.indexOf(pushType) < 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||||
|
'Push to ' + pushTypes + ' is not supported');
|
||||||
|
}
|
||||||
|
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 valid push types.
|
||||||
|
* @returns {Array} An array of valid push types
|
||||||
|
*/
|
||||||
|
ParsePushAdapter.prototype.getValidPushTypes = function() {
|
||||||
|
return this.validPushTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParsePushAdapter.prototype.send = function(data, installations) {
|
||||||
|
let deviceMap = classifyInstallation(installations, this.validPushTypes);
|
||||||
|
let sendPromises = [];
|
||||||
|
for (let pushType in deviceMap) {
|
||||||
|
let 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)
|
||||||
|
* @returns {Object} A map whose key is device type and value is an array of device
|
||||||
|
*/
|
||||||
|
function classifyInstallation(installations, validPushTypes) {
|
||||||
|
// Init deviceTokenMap, create a empty array for each valid pushType
|
||||||
|
let deviceMap = {};
|
||||||
|
for (let validPushType of validPushTypes) {
|
||||||
|
deviceMap[validPushType] = [];
|
||||||
|
}
|
||||||
|
for (let installation of installations) {
|
||||||
|
// No deviceToken, ignore
|
||||||
|
if (!installation.deviceToken) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let pushType = installation.deviceType;
|
||||||
|
if (deviceMap[pushType]) {
|
||||||
|
deviceMap[pushType].push({
|
||||||
|
deviceToken: installation.deviceToken,
|
||||||
|
appIdentifier: installation.appIdentifier
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('Unknown push type from installation %j', installation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deviceMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||||
|
ParsePushAdapter.classifyInstallation = classifyInstallation;
|
||||||
|
}
|
||||||
|
module.exports = ParsePushAdapter;
|
||||||
17
src/Adapters/Push/PushAdapter.js
Normal file
17
src/Adapters/Push/PushAdapter.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Push Adapter
|
||||||
|
//
|
||||||
|
// Allows you to change the push notification mechanism.
|
||||||
|
//
|
||||||
|
// Adapter classes must implement the following functions:
|
||||||
|
// * getValidPushTypes()
|
||||||
|
// * send(devices, installations)
|
||||||
|
//
|
||||||
|
// Default is ParsePushAdapter, which uses GCM for
|
||||||
|
// android push and APNS for ios push.
|
||||||
|
export class PushAdapter {
|
||||||
|
send(devices, installations) { }
|
||||||
|
|
||||||
|
getValidPushTypes() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PushAdapter;
|
||||||
@@ -1,28 +1,52 @@
|
|||||||
// push.js
|
import { Parse } from 'parse/node';
|
||||||
|
import PromiseRouter from '../PromiseRouter';
|
||||||
|
import rest from '../rest';
|
||||||
|
|
||||||
var Parse = require('parse/node').Parse,
|
export class PushController {
|
||||||
PromiseRouter = require('./PromiseRouter'),
|
|
||||||
rest = require('./rest');
|
|
||||||
|
|
||||||
var validPushTypes = ['ios', 'android'];
|
constructor(pushAdapter) {
|
||||||
|
this._pushAdapter = pushAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
function handlePushWithoutQueue(req) {
|
handlePOST(req) {
|
||||||
validateMasterKey(req);
|
if (!this._pushAdapter) {
|
||||||
var where = getQueryCondition(req);
|
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||||
validateDeviceType(where);
|
'Push adapter is not availabe');
|
||||||
// Replace the expiration_time with a valid Unix epoch milliseconds time
|
}
|
||||||
req.body['expiration_time'] = getExpirationTime(req);
|
|
||||||
return rest.find(req.config, req.auth, '_Installation', where).then(function(response) {
|
validateMasterKey(req);
|
||||||
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE,
|
var where = getQueryCondition(req);
|
||||||
'This path is not implemented yet.');
|
var pushAdapter = this._pushAdapter;
|
||||||
});
|
validatePushType(where, pushAdapter.getValidPushTypes());
|
||||||
|
// Replace the expiration_time with a valid Unix epoch milliseconds time
|
||||||
|
req.body['expiration_time'] = getExpirationTime(req);
|
||||||
|
// TODO: If the req can pass the checking, we return immediately instead of waiting
|
||||||
|
// pushes to be sent. We probably change this behaviour in the future.
|
||||||
|
rest.find(req.config, req.auth, '_Installation', where).then(function(response) {
|
||||||
|
return pushAdapter.send(req.body, response.results);
|
||||||
|
});
|
||||||
|
return Parse.Promise.as({
|
||||||
|
response: {
|
||||||
|
'result': true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getExpressRouter() {
|
||||||
|
var router = new PromiseRouter();
|
||||||
|
router.route('POST','/push', (req) => {
|
||||||
|
return this.handlePOST(req);
|
||||||
|
});
|
||||||
|
return router;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the deviceType parameter in qury condition is valid or not.
|
* Check whether the deviceType parameter in qury condition is valid or not.
|
||||||
* @param {Object} where A query condition
|
* @param {Object} where A query condition
|
||||||
|
* @param {Array} validPushTypes An array of valid push types(string)
|
||||||
*/
|
*/
|
||||||
function validateDeviceType(where) {
|
function validatePushType(where, validPushTypes) {
|
||||||
var where = where || {};
|
var where = where || {};
|
||||||
var deviceTypeField = where.deviceType || {};
|
var deviceTypeField = where.deviceType || {};
|
||||||
var deviceTypes = [];
|
var deviceTypes = [];
|
||||||
@@ -109,16 +133,11 @@ function validateMasterKey(req) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var router = new PromiseRouter();
|
|
||||||
router.route('POST','/push', handlePushWithoutQueue);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
router: router
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||||
module.exports.getQueryCondition = getQueryCondition;
|
PushController.getQueryCondition = getQueryCondition;
|
||||||
module.exports.validateMasterKey = validateMasterKey;
|
PushController.validateMasterKey = validateMasterKey;
|
||||||
module.exports.getExpirationTime = getExpirationTime;
|
PushController.getExpirationTime = getExpirationTime;
|
||||||
module.exports.validateDeviceType = validateDeviceType;
|
PushController.validatePushType = validatePushType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default PushController;
|
||||||
92
src/GCM.js
92
src/GCM.js
@@ -1,47 +1,66 @@
|
|||||||
var Parse = require('parse/node').Parse;
|
"use strict";
|
||||||
var gcm = require('node-gcm');
|
|
||||||
var randomstring = require('randomstring');
|
|
||||||
|
|
||||||
var GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
|
const Parse = require('parse/node').Parse;
|
||||||
var GCMRegistrationTokensMax = 1000;
|
const gcm = require('node-gcm');
|
||||||
|
const randomstring = require('randomstring');
|
||||||
|
|
||||||
function GCM(apiKey) {
|
const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
|
||||||
this.sender = new gcm.Sender(apiKey);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send gcm request.
|
* Send gcm request.
|
||||||
* @param {Object} data The data we need to send, the format is the same with api request body
|
* @param {Object} data The data we need to send, the format is the same with api request body
|
||||||
* @param {Array} registrationTokens A array of registration tokens
|
* @param {Array} devices A array of devices
|
||||||
* @returns {Object} A promise which is resolved after we get results from gcm
|
* @returns {Object} A promise which is resolved after we get results from gcm
|
||||||
*/
|
*/
|
||||||
GCM.prototype.send = function (data, registrationTokens) {
|
GCM.prototype.send = function(data, devices) {
|
||||||
if (registrationTokens.length >= GCMRegistrationTokensMax) {
|
let pushId = randomstring.generate({
|
||||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
|
||||||
'Too many registration tokens for a GCM request.');
|
|
||||||
}
|
|
||||||
var pushId = randomstring.generate({
|
|
||||||
length: 10,
|
length: 10,
|
||||||
charset: 'alphanumeric'
|
charset: 'alphanumeric'
|
||||||
});
|
});
|
||||||
var timeStamp = Date.now();
|
let timeStamp = Date.now();
|
||||||
var expirationTime;
|
let expirationTime;
|
||||||
// We handle the expiration_time convertion in push.js, so expiration_time is a valid date
|
// We handle the expiration_time convertion in push.js, so expiration_time is a valid date
|
||||||
// in Unix epoch time in milliseconds here
|
// in Unix epoch time in milliseconds here
|
||||||
if (data['expiration_time']) {
|
if (data['expiration_time']) {
|
||||||
expirationTime = data['expiration_time'];
|
expirationTime = data['expiration_time'];
|
||||||
}
|
}
|
||||||
// Generate gcm payload
|
// Generate gcm payload
|
||||||
var gcmPayload = generateGCMPayload(data.data, pushId, timeStamp, expirationTime);
|
let gcmPayload = generateGCMPayload(data.data, pushId, timeStamp, expirationTime);
|
||||||
// Make and send gcm request
|
// Make and send gcm request
|
||||||
var message = new gcm.Message(gcmPayload);
|
let message = new gcm.Message(gcmPayload);
|
||||||
var promise = new Parse.Promise();
|
|
||||||
this.sender.send(message, { registrationTokens: registrationTokens }, 5, function (error, response) {
|
let sendPromises = [];
|
||||||
// TODO: Use the response from gcm to generate and save push report
|
// For android, we can only have 1000 recepients per send, so we need to slice devices to
|
||||||
// TODO: If gcm returns some deviceTokens are invalid, set tombstone for the installation
|
// chunk if necessary
|
||||||
promise.resolve();
|
let chunkDevices = sliceDevices(devices, GCMRegistrationTokensMax);
|
||||||
});
|
for (let chunkDevice of chunkDevices) {
|
||||||
return promise;
|
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();
|
||||||
|
});
|
||||||
|
sendPromises.push(sendPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Parse.Promise.when(sendPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,19 +71,19 @@ GCM.prototype.send = function (data, registrationTokens) {
|
|||||||
* @param {Number|undefined} expirationTime A number whose format is the Unix Epoch or undefined
|
* @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
|
* @returns {Object} A promise which is resolved after we get results from gcm
|
||||||
*/
|
*/
|
||||||
var generateGCMPayload = function(coreData, pushId, timeStamp, expirationTime) {
|
function generateGCMPayload(coreData, pushId, timeStamp, expirationTime) {
|
||||||
var payloadData = {
|
let payloadData = {
|
||||||
'time': new Date(timeStamp).toISOString(),
|
'time': new Date(timeStamp).toISOString(),
|
||||||
'push_id': pushId,
|
'push_id': pushId,
|
||||||
'data': JSON.stringify(coreData)
|
'data': JSON.stringify(coreData)
|
||||||
}
|
}
|
||||||
var payload = {
|
let payload = {
|
||||||
priority: 'normal',
|
priority: 'normal',
|
||||||
data: payloadData
|
data: payloadData
|
||||||
};
|
};
|
||||||
if (expirationTime) {
|
if (expirationTime) {
|
||||||
// The timeStamp and expiration is in milliseconds but gcm requires second
|
// The timeStamp and expiration is in milliseconds but gcm requires second
|
||||||
var timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
|
let timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
|
||||||
if (timeToLive < 0) {
|
if (timeToLive < 0) {
|
||||||
timeToLive = 0;
|
timeToLive = 0;
|
||||||
}
|
}
|
||||||
@@ -76,7 +95,22 @@ var generateGCMPayload = function(coreData, pushId, timeStamp, expirationTime) {
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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') {
|
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||||
GCM.generateGCMPayload = generateGCMPayload;
|
GCM.generateGCMPayload = generateGCMPayload;
|
||||||
|
GCM.sliceDevices = sliceDevices;
|
||||||
}
|
}
|
||||||
module.exports = GCM;
|
module.exports = GCM;
|
||||||
|
|||||||
20
src/index.js
20
src/index.js
@@ -13,9 +13,12 @@ var batch = require('./batch'),
|
|||||||
|
|
||||||
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
|
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
|
||||||
import { S3Adapter } from './Adapters/Files/S3Adapter';
|
import { S3Adapter } from './Adapters/Files/S3Adapter';
|
||||||
|
|
||||||
import { FilesController } from './Controllers/FilesController';
|
import { FilesController } from './Controllers/FilesController';
|
||||||
|
|
||||||
|
import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
|
||||||
|
import { PushController } from './Controllers/PushController';
|
||||||
|
|
||||||
|
|
||||||
// Mutate the Parse object to add the Cloud Code handlers
|
// Mutate the Parse object to add the Cloud Code handlers
|
||||||
addParseCloud();
|
addParseCloud();
|
||||||
|
|
||||||
@@ -41,6 +44,8 @@ addParseCloud();
|
|||||||
// "dotNetKey": optional key from Parse dashboard
|
// "dotNetKey": optional key from Parse dashboard
|
||||||
// "restAPIKey": optional key from Parse dashboard
|
// "restAPIKey": optional key from Parse dashboard
|
||||||
// "javascriptKey": optional key from Parse dashboard
|
// "javascriptKey": optional key from Parse dashboard
|
||||||
|
// "push": optional key from configure push
|
||||||
|
|
||||||
function ParseServer(args) {
|
function ParseServer(args) {
|
||||||
if (!args.appId || !args.masterKey) {
|
if (!args.appId || !args.masterKey) {
|
||||||
throw 'You must provide an appId and masterKey!';
|
throw 'You must provide an appId and masterKey!';
|
||||||
@@ -50,8 +55,18 @@ function ParseServer(args) {
|
|||||||
DatabaseAdapter.setAdapter(args.databaseAdapter);
|
DatabaseAdapter.setAdapter(args.databaseAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make files adapter
|
||||||
let filesAdapter = args.filesAdapter || new GridStoreAdapter();
|
let filesAdapter = args.filesAdapter || new GridStoreAdapter();
|
||||||
|
|
||||||
|
// Make push adapter
|
||||||
|
let pushConfig = args.push;
|
||||||
|
let pushAdapter;
|
||||||
|
if (pushConfig && pushConfig.adapter) {
|
||||||
|
pushAdapter = pushConfig.adapter;
|
||||||
|
} else if (pushConfig) {
|
||||||
|
pushAdapter = new ParsePushAdapter(pushConfig)
|
||||||
|
}
|
||||||
|
|
||||||
if (args.databaseURI) {
|
if (args.databaseURI) {
|
||||||
DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI);
|
DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI);
|
||||||
}
|
}
|
||||||
@@ -117,13 +132,14 @@ function ParseServer(args) {
|
|||||||
router.merge(require('./sessions'));
|
router.merge(require('./sessions'));
|
||||||
router.merge(require('./roles'));
|
router.merge(require('./roles'));
|
||||||
router.merge(require('./analytics'));
|
router.merge(require('./analytics'));
|
||||||
router.merge(require('./push').router);
|
|
||||||
router.merge(require('./installations'));
|
router.merge(require('./installations'));
|
||||||
router.merge(require('./functions'));
|
router.merge(require('./functions'));
|
||||||
router.merge(require('./schemas'));
|
router.merge(require('./schemas'));
|
||||||
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
|
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
|
||||||
router.merge(require('./global_config'));
|
router.merge(require('./global_config'));
|
||||||
}
|
}
|
||||||
|
let pushController = new PushController(pushAdapter);
|
||||||
|
router.merge(pushController.getExpressRouter());
|
||||||
|
|
||||||
batch.mountOnto(router);
|
batch.mountOnto(router);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user