Merge pull request #1004 from ParsePlatform/flovilmart.PushStatus
Push Status API
This commit is contained in:
@@ -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)];
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
'use strict';
|
||||
describe('Parse.Push', () => {
|
||||
it('should properly send push', (done) => {
|
||||
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 +59,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();
|
||||
});
|
||||
|
||||
@@ -3,6 +3,30 @@ var PushController = require('../src/Controllers/PushController').PushController
|
||||
|
||||
var Config = require('../src/Config');
|
||||
|
||||
const successfulTransmissions = function(body, installations) {
|
||||
|
||||
let promises = installations.map((device) => {
|
||||
return Promise.resolve({
|
||||
transmitted: true,
|
||||
device: device,
|
||||
})
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
const successfulIOS = function(body, installations) {
|
||||
|
||||
let promises = installations.map((device) => {
|
||||
return Promise.resolve({
|
||||
transmitted: device.deviceType == "ios",
|
||||
device: device,
|
||||
})
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
describe('PushController', () => {
|
||||
it('can validate device type when no device type is set', (done) => {
|
||||
// Make query condition
|
||||
@@ -105,9 +129,9 @@ describe('PushController', () => {
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('properly increment badges', (done) => {
|
||||
|
||||
|
||||
var payload = {data:{
|
||||
alert: "Hello World!",
|
||||
badge: "Increment",
|
||||
@@ -122,7 +146,7 @@ describe('PushController', () => {
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
|
||||
while(installations.length != 15) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
@@ -130,7 +154,7 @@ describe('PushController', () => {
|
||||
installation.set("deviceType", "android");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.data.badge;
|
||||
@@ -142,23 +166,20 @@ describe('PushController', () => {
|
||||
expect(installation.badge).toBeUndefined();
|
||||
}
|
||||
})
|
||||
return Promise.resolve({
|
||||
body: body,
|
||||
installations: installations
|
||||
})
|
||||
return successfulTransmissions(body, installations);
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios", "android"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
Parse.Object.saveAll(installations).then((installations) => {
|
||||
Parse.Object.saveAll(installations).then((installations) => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then((result) => {
|
||||
done();
|
||||
@@ -167,11 +188,11 @@ describe('PushController', () => {
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('properly set badges to 1', (done) => {
|
||||
|
||||
|
||||
var payload = {data: {
|
||||
alert: "Hello World!",
|
||||
badge: 1,
|
||||
@@ -186,7 +207,7 @@ describe('PushController', () => {
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.data.badge;
|
||||
@@ -194,23 +215,20 @@ describe('PushController', () => {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(1).toEqual(installation.badge);
|
||||
})
|
||||
return Promise.resolve({
|
||||
body: body,
|
||||
installations: installations
|
||||
})
|
||||
return successfulTransmissions(body, installations);
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
Parse.Object.saveAll(installations).then((installations) => {
|
||||
Parse.Object.saveAll(installations).then((installations) => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then((result) => {
|
||||
done();
|
||||
@@ -219,29 +237,106 @@ describe('PushController', () => {
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
it('should support full RESTQuery for increment', (done) => {
|
||||
var payload = {data: {
|
||||
|
||||
it('properly creates _PushStatus', (done) => {
|
||||
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
while(installations.length != 15) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("deviceType", "android");
|
||||
installations.push(installation);
|
||||
}
|
||||
var payload = {data: {
|
||||
alert: "Hello World!",
|
||||
badge: 'Increment',
|
||||
badge: 1,
|
||||
}}
|
||||
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
return Promise.resolve();
|
||||
return successfulIOS(body, installations);
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
Parse.Object.saveAll(installations).then(() => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then((result) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
}).then(() => {
|
||||
let query = new Parse.Query('_PushStatus');
|
||||
return query.find({useMasterKey: true});
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
let result = results[0];
|
||||
expect(result.createdAt instanceof Date).toBe(true);
|
||||
expect(result.get('source')).toEqual('rest');
|
||||
expect(result.get('query')).toEqual(JSON.stringify({}));
|
||||
expect(result.get('payload')).toEqual(payload.data);
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
expect(result.get('numSent')).toEqual(10);
|
||||
expect(result.get('sentPerType')).toEqual({
|
||||
'ios': 10 // 10 ios
|
||||
});
|
||||
expect(result.get('numFailed')).toEqual(5);
|
||||
expect(result.get('failedPerType')).toEqual({
|
||||
'android': 5 // android
|
||||
});
|
||||
// Try to get it without masterKey
|
||||
let query = new Parse.Query('_PushStatus');
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should support full RESTQuery for increment', (done) => {
|
||||
var payload = {data: {
|
||||
alert: "Hello World!",
|
||||
badge: 'Increment',
|
||||
}}
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
return successfulTransmissions(body, installations);
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ["ios"];
|
||||
}
|
||||
}
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
let where = {
|
||||
'deviceToken': {
|
||||
'$inQuery': {
|
||||
|
||||
33
src/APNS.js
33
src/APNS.js
@@ -66,6 +66,13 @@ function APNS(args) {
|
||||
});
|
||||
|
||||
conn.on('transmitted', function(notification, device) {
|
||||
if (device.callback) {
|
||||
device.callback({
|
||||
notification: notification,
|
||||
transmitted: true,
|
||||
device: device
|
||||
});
|
||||
}
|
||||
console.log('APNS Connection %d Notification transmitted to %s', conn.index, device.token.toString('hex'));
|
||||
});
|
||||
|
||||
@@ -91,11 +98,15 @@ APNS.prototype.send = function(data, devices) {
|
||||
let coreData = data.data;
|
||||
let expirationTime = data['expiration_time'];
|
||||
let notification = generateNotification(coreData, expirationTime);
|
||||
for (let device of devices) {
|
||||
|
||||
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) {
|
||||
continue;
|
||||
return Promise.resolve({
|
||||
transmitted: false,
|
||||
result: {error: 'No connection available'}
|
||||
});
|
||||
}
|
||||
let conn = this.conns[qualifiedConnIndexs[0]];
|
||||
let apnDevice = new apn.Device(device.deviceToken);
|
||||
@@ -104,13 +115,15 @@ APNS.prototype.send = function(data, devices) {
|
||||
if (device.appIdentifier) {
|
||||
apnDevice.appIdentifier = device.appIdentifier;
|
||||
}
|
||||
conn.pushNotification(notification, apnDevice);
|
||||
}
|
||||
return Parse.Promise.as();
|
||||
return new Promise((resolve, reject) => {
|
||||
apnDevice.callback = resolve;
|
||||
conn.pushNotification(notification, apnDevice);
|
||||
});
|
||||
});
|
||||
return Parse.Promise.when(promises);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -133,7 +146,13 @@ function handleTransmissionError(conns, errCode, notification, apnDevice) {
|
||||
}
|
||||
// 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);
|
||||
if (apnDevice.callback) {
|
||||
apnDevice.callback({
|
||||
response: {error: `APNS can not find vaild connection for ${apnDevice.token}`, code: errCode},
|
||||
status: errCode,
|
||||
transmitted: false
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -10,6 +10,9 @@ import PushAdapter from './PushAdapter';
|
||||
import { classifyInstallations } from './PushAdapterUtils';
|
||||
|
||||
export class ParsePushAdapter extends PushAdapter {
|
||||
|
||||
supportsPushTracking = true;
|
||||
|
||||
constructor(pushConfig = {}) {
|
||||
super(pushConfig);
|
||||
this.validPushTypes = ['ios', 'android'];
|
||||
@@ -19,7 +22,7 @@ export class ParsePushAdapter extends PushAdapter {
|
||||
immediatePush: true
|
||||
};
|
||||
let pushTypes = Object.keys(pushConfig);
|
||||
|
||||
|
||||
for (let pushType of pushTypes) {
|
||||
if (this.validPushTypes.indexOf(pushType) < 0) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
@@ -35,7 +38,7 @@ export class ParsePushAdapter extends PushAdapter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getValidPushTypes() {
|
||||
return this.validPushTypes;
|
||||
}
|
||||
@@ -43,18 +46,21 @@ export class ParsePushAdapter extends PushAdapter {
|
||||
static classifyInstallations(installations, validTypes) {
|
||||
return classifyInstallations(installations, validTypes)
|
||||
}
|
||||
|
||||
|
||||
send(data, installations) {
|
||||
let deviceMap = classifyInstallations(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;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
//
|
||||
// Adapter classes must implement the following functions:
|
||||
// * getValidPushTypes()
|
||||
// * send(devices, installations)
|
||||
// * send(devices, installations, pushStatus)
|
||||
//
|
||||
// Default is ParsePushAdapter, which uses GCM for
|
||||
// android push and APNS for ios push.
|
||||
|
||||
export class PushAdapter {
|
||||
send(devices, installations) { }
|
||||
send(devices, installations, pushStatus) { }
|
||||
|
||||
/**
|
||||
* Get an array of valid push types.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PushAdapter } from '../Adapters/Push/PushAdapter';
|
||||
import deepcopy from 'deepcopy';
|
||||
import features from '../features';
|
||||
import RestQuery from '../RestQuery';
|
||||
import pushStatusHandler from '../pushStatusHandler';
|
||||
|
||||
const FEATURE_NAME = 'push';
|
||||
const UNSUPPORTED_BADGE_KEY = "unsupported";
|
||||
@@ -38,7 +39,7 @@ export class PushController extends AdaptableController {
|
||||
}
|
||||
}
|
||||
|
||||
sendPush(body = {}, where = {}, config, auth) {
|
||||
sendPush(body = {}, where = {}, config, auth, wait) {
|
||||
var pushAdapter = this.adapter;
|
||||
if (!pushAdapter) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
@@ -65,7 +66,7 @@ export class PushController extends AdaptableController {
|
||||
}
|
||||
let updateWhere = deepcopy(where);
|
||||
|
||||
badgeUpdate = () => {
|
||||
badgeUpdate = () => {
|
||||
let badgeQuery = new RestQuery(config, auth, '_Installation', updateWhere);
|
||||
return badgeQuery.buildRestWhere().then(() => {
|
||||
let restWhere = deepcopy(badgeQuery.restWhere);
|
||||
@@ -81,38 +82,58 @@ export class PushController extends AdaptableController {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return badgeUpdate().then(() => {
|
||||
let pushStatus = pushStatusHandler(config);
|
||||
return Promise.resolve().then(() => {
|
||||
return pushStatus.setInitial(body, where);
|
||||
}).then(() => {
|
||||
return badgeUpdate();
|
||||
}).then(() => {
|
||||
return rest.find(config, auth, '_Installation', where);
|
||||
}).then((response) => {
|
||||
if (body.data && body.data.badge && body.data.badge == "Increment") {
|
||||
// Collect the badges to reduce the # of calls
|
||||
let badgeInstallationsMap = response.results.reduce((map, installation) => {
|
||||
let badge = installation.badge;
|
||||
if (installation.deviceType != "ios") {
|
||||
badge = UNSUPPORTED_BADGE_KEY;
|
||||
}
|
||||
map[badge+''] = map[badge+''] || [];
|
||||
map[badge+''].push(installation);
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
// Map the on the badges count and return the send result
|
||||
let promises = Object.keys(badgeInstallationsMap).map((badge) => {
|
||||
let payload = deepcopy(body);
|
||||
if (badge == UNSUPPORTED_BADGE_KEY) {
|
||||
delete payload.data.badge;
|
||||
} else {
|
||||
payload.data.badge = parseInt(badge);
|
||||
}
|
||||
return pushAdapter.send(payload, badgeInstallationsMap[badge]);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
return pushAdapter.send(body, response.results);
|
||||
pushStatus.setRunning();
|
||||
return this.sendToAdapter(body, response.results, pushStatus, config);
|
||||
}).then((results) => {
|
||||
return pushStatus.complete(results);
|
||||
});
|
||||
}
|
||||
|
||||
sendToAdapter(body, installations, pushStatus, config) {
|
||||
if (body.data && body.data.badge && body.data.badge == "Increment") {
|
||||
// Collect the badges to reduce the # of calls
|
||||
let badgeInstallationsMap = installations.reduce((map, installation) => {
|
||||
let badge = installation.badge;
|
||||
if (installation.deviceType != "ios") {
|
||||
badge = UNSUPPORTED_BADGE_KEY;
|
||||
}
|
||||
map[badge+''] = map[badge+''] || [];
|
||||
map[badge+''].push(installation);
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
// Map the on the badges count and return the send result
|
||||
let promises = Object.keys(badgeInstallationsMap).map((badge) => {
|
||||
let payload = deepcopy(body);
|
||||
if (badge == UNSUPPORTED_BADGE_KEY) {
|
||||
delete payload.data.badge;
|
||||
} else {
|
||||
payload.data.badge = parseInt(badge);
|
||||
}
|
||||
return this.adapter.send(payload, badgeInstallationsMap[badge]);
|
||||
});
|
||||
// Flatten the promises results
|
||||
return Promise.all(promises).then((results) => {
|
||||
if (Array.isArray(results)) {
|
||||
return Promise.resolve(results.reduce((memo, result) => {
|
||||
return memo.concat(result);
|
||||
},[]));
|
||||
} else {
|
||||
return Promise.resolve(results);
|
||||
}
|
||||
})
|
||||
}
|
||||
return this.adapter.send(body, installations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get expiration time from the request body.
|
||||
* @param {Object} request A request object
|
||||
|
||||
92
src/GCM.js
92
src/GCM.js
@@ -22,8 +22,29 @@ function GCM(args) {
|
||||
* @returns {Object} A promise which is resolved after we get results from gcm
|
||||
*/
|
||||
GCM.prototype.send = function(data, devices) {
|
||||
let pushId = cryptoUtils.newObjectId();
|
||||
let timeStamp = Date.now();
|
||||
// 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,33 +52,51 @@ GCM.prototype.send = function(data, devices) {
|
||||
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();
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,10 +107,9 @@ GCM.prototype.send = function(data, devices) {
|
||||
* @param {Number|undefined} expirationTime A number whose format is the Unix Epoch or undefined
|
||||
* @returns {Object} A promise which is resolved after we get results from gcm
|
||||
*/
|
||||
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 = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// An object that encapsulates everything we need to run a 'find'
|
||||
// operation, encoded in the REST API format.
|
||||
|
||||
var Schema = require('./Schema');
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
import { default as FilesController } from './Controllers/FilesController';
|
||||
@@ -170,7 +171,7 @@ RestQuery.prototype.redirectClassNameForKey = function() {
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestQuery.prototype.validateClientClassCreation = function() {
|
||||
let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product'];
|
||||
let sysClass = Schema.systemClasses;
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||
&& sysClass.indexOf(this.className) === -1) {
|
||||
return this.config.database.collectionExists(this.className).then((hasClass) => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// This could be either a "create" or an "update".
|
||||
|
||||
import cache from './cache';
|
||||
var Schema = require('./Schema');
|
||||
var deepcopy = require('deepcopy');
|
||||
|
||||
var Auth = require('./Auth');
|
||||
@@ -108,7 +109,7 @@ RestWrite.prototype.getUserAndRoleACL = function() {
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestWrite.prototype.validateClientClassCreation = function() {
|
||||
let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product'];
|
||||
let sysClass = Schema.systemClasses;
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||
&& sysClass.indexOf(this.className) === -1) {
|
||||
return this.config.database.collectionExists(this.className).then((hasClass) => {
|
||||
|
||||
@@ -67,7 +67,22 @@ var defaultColumns = {
|
||||
"icon": {type:'File'},
|
||||
"order": {type:'Number'},
|
||||
"title": {type:'String'},
|
||||
"subtitle": {type:'String'},
|
||||
"subtitle": {type:'String'},
|
||||
},
|
||||
_PushStatus: {
|
||||
"pushTime": {type:'String'},
|
||||
"source": {type:'String'}, // rest or webui
|
||||
"query": {type:'String'}, // the stringified JSON query
|
||||
"payload": {type:'Object'}, // the JSON payload,
|
||||
"title": {type:'String'},
|
||||
"expiry": {type:'Number'},
|
||||
"status": {type:'String'},
|
||||
"numSent": {type:'Number'},
|
||||
"numFailed": {type:'Number'},
|
||||
"pushHash": {type:'String'},
|
||||
"errorMessage": {type:'Object'},
|
||||
"sentPerType": {type:'Object'},
|
||||
"failedPerType":{type:'Object'},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -76,6 +91,8 @@ var requiredColumns = {
|
||||
_Role: ["name", "ACL"]
|
||||
}
|
||||
|
||||
const systemClasses = ['_User', '_Installation', '_Role', '_Session', '_Product'];
|
||||
|
||||
// 10 alpha numberic chars + uppercase
|
||||
const userIdRegex = /^[a-zA-Z0-9]{10}$/;
|
||||
// Anything that start with role
|
||||
@@ -127,13 +144,8 @@ function validateCLP(perms) {
|
||||
var joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
|
||||
var classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
|
||||
function classNameIsValid(className) {
|
||||
return (
|
||||
className === '_User' ||
|
||||
className === '_Installation' ||
|
||||
className === '_Session' ||
|
||||
return (systemClasses.indexOf(className) > -1 ||
|
||||
className === '_SCHEMA' || //TODO: remove this, as _SCHEMA is not a valid class name for storing Parse Objects.
|
||||
className === '_Role' ||
|
||||
className === '_Product' ||
|
||||
joinClassRegex.test(className) ||
|
||||
//Class names have the same constraints as field names, but also allow the previous additional names.
|
||||
fieldNameIsValid(className)
|
||||
@@ -284,7 +296,7 @@ class Schema {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
updateClass(className, submittedFields, classLevelPermissions, database) {
|
||||
if (!this.data[className]) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||
@@ -299,7 +311,7 @@ class Schema {
|
||||
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let newSchema = buildMergedSchemaObject(existingFields, submittedFields);
|
||||
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(newSchema, className, classLevelPermissions);
|
||||
if (!mongoObject.result) {
|
||||
@@ -327,7 +339,7 @@ class Schema {
|
||||
});
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then(() => {
|
||||
.then(() => {
|
||||
return this.setPermissions(className, classLevelPermissions)
|
||||
})
|
||||
.then(() => { return mongoSchemaToSchemaAPIResponse(mongoObject.result) });
|
||||
@@ -697,7 +709,7 @@ function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPe
|
||||
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
validateCLP(classLevelPermissions);
|
||||
if (typeof classLevelPermissions !== 'undefined') {
|
||||
mongoObject._metadata = mongoObject._metadata || {};
|
||||
@@ -886,11 +898,11 @@ function mongoSchemaToSchemaAPIResponse(schema) {
|
||||
className: schema._id,
|
||||
fields: mongoSchemaAPIResponseFields(schema),
|
||||
};
|
||||
|
||||
|
||||
let classLevelPermissions = DefaultClassLevelPermissions;
|
||||
if (schema._metadata && schema._metadata.class_permissions) {
|
||||
classLevelPermissions = Object.assign(classLevelPermissions, schema._metadata.class_permissions);
|
||||
}
|
||||
}
|
||||
result.classLevelPermissions = classLevelPermissions;
|
||||
return result;
|
||||
}
|
||||
@@ -903,4 +915,5 @@ module.exports = {
|
||||
buildMergedSchemaObject: buildMergedSchemaObject,
|
||||
mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType,
|
||||
mongoSchemaToSchemaAPIResponse,
|
||||
systemClasses,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* @flow */
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
import { randomBytes, createHash } from 'crypto';
|
||||
|
||||
// Returns a new random hex string of the given even size.
|
||||
export function randomHexString(size: number): string {
|
||||
@@ -44,3 +44,7 @@ export function newObjectId(): string {
|
||||
export function newToken(): string {
|
||||
return randomHexString(32);
|
||||
}
|
||||
|
||||
export function md5Hash(string: string): string {
|
||||
return createHash('md5').update(string).digest('hex');
|
||||
}
|
||||
|
||||
90
src/pushStatusHandler.js
Normal file
90
src/pushStatusHandler.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { md5Hash, newObjectId } from './cryptoUtils';
|
||||
|
||||
export default function pushStatusHandler(config) {
|
||||
|
||||
let initialPromise;
|
||||
let pushStatus;
|
||||
|
||||
let collection = function() {
|
||||
return config.database.adaptiveCollection('_PushStatus');
|
||||
}
|
||||
|
||||
let setInitial = function(body, where, options = {source: 'rest'}) {
|
||||
let now = new Date();
|
||||
let object = {
|
||||
objectId: newObjectId(),
|
||||
pushTime: now.toISOString(),
|
||||
_created_at: now,
|
||||
query: JSON.stringify(where),
|
||||
payload: body.data,
|
||||
source: options.source,
|
||||
title: options.title,
|
||||
expiry: body.expiration_time,
|
||||
status: "pending",
|
||||
numSent: 0,
|
||||
pushHash: md5Hash(JSON.stringify(body.data)),
|
||||
// lockdown!
|
||||
_wperm: [],
|
||||
_rperm: []
|
||||
}
|
||||
initialPromise = collection().then((collection) => {
|
||||
return collection.insertOne(object);
|
||||
}).then((res) => {
|
||||
pushStatus = {
|
||||
objectId: object.objectId
|
||||
};
|
||||
return Promise.resolve(pushStatus);
|
||||
})
|
||||
return initialPromise;
|
||||
}
|
||||
|
||||
let setRunning = function() {
|
||||
return initialPromise.then(() => {
|
||||
return collection();
|
||||
}).then((collection) => {
|
||||
return collection.updateOne({status:"pending", objectId: pushStatus.objectId}, {$set: {status: "running"}});
|
||||
});
|
||||
}
|
||||
|
||||
let complete = function(results) {
|
||||
let update = {
|
||||
status: 'succeeded',
|
||||
numSent: 0,
|
||||
numFailed: 0,
|
||||
};
|
||||
if (Array.isArray(results)) {
|
||||
results.reduce((memo, result) => {
|
||||
// Cannot handle that
|
||||
if (!result.device || !result.device.deviceType) {
|
||||
return memo;
|
||||
}
|
||||
let deviceType = result.device.deviceType;
|
||||
if (result.transmitted)
|
||||
{
|
||||
memo.numSent++;
|
||||
memo.sentPerType = memo.sentPerType || {};
|
||||
memo.sentPerType[deviceType] = memo.sentPerType[deviceType] || 0;
|
||||
memo.sentPerType[deviceType]++;
|
||||
} else {
|
||||
memo.numFailed++;
|
||||
memo.failedPerType = memo.failedPerType || {};
|
||||
memo.failedPerType[deviceType] = memo.failedPerType[deviceType] || 0;
|
||||
memo.failedPerType[deviceType]++;
|
||||
}
|
||||
return memo;
|
||||
}, update);
|
||||
}
|
||||
|
||||
return initialPromise.then(() => {
|
||||
return collection();
|
||||
}).then((collection) => {
|
||||
return collection.updateOne({status:"running", objectId: pushStatus.objectId}, {$set: update});
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
setInitial,
|
||||
setRunning,
|
||||
complete
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user