Merge pull request #1004 from ParsePlatform/flovilmart.PushStatus
Push Status API
This commit is contained in:
@@ -23,17 +23,15 @@ describe('GCM', () => {
|
|||||||
var data = {
|
var data = {
|
||||||
'alert': 'alert'
|
'alert': 'alert'
|
||||||
};
|
};
|
||||||
var pushId = 1;
|
|
||||||
var timeStamp = 1454538822113;
|
var timeStamp = 1454538822113;
|
||||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
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.priority).toEqual('normal');
|
||||||
expect(payload.timeToLive).toEqual(undefined);
|
expect(payload.timeToLive).toEqual(undefined);
|
||||||
var dataFromPayload = payload.data;
|
var dataFromPayload = payload.data;
|
||||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
|
||||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||||
expect(dataFromUser).toEqual(data);
|
expect(dataFromUser).toEqual(data);
|
||||||
done();
|
done();
|
||||||
@@ -44,18 +42,16 @@ describe('GCM', () => {
|
|||||||
var data = {
|
var data = {
|
||||||
'alert': 'alert'
|
'alert': 'alert'
|
||||||
};
|
};
|
||||||
var pushId = 1;
|
|
||||||
var timeStamp = 1454538822113;
|
var timeStamp = 1454538822113;
|
||||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
var timeStampISOStr = new Date(timeStamp).toISOString();
|
||||||
var expirationTime = 1454538922113
|
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.priority).toEqual('normal');
|
||||||
expect(payload.timeToLive).toEqual(Math.floor((expirationTime - timeStamp) / 1000));
|
expect(payload.timeToLive).toEqual(Math.floor((expirationTime - timeStamp) / 1000));
|
||||||
var dataFromPayload = payload.data;
|
var dataFromPayload = payload.data;
|
||||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
|
||||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||||
expect(dataFromUser).toEqual(data);
|
expect(dataFromUser).toEqual(data);
|
||||||
done();
|
done();
|
||||||
@@ -66,18 +62,16 @@ describe('GCM', () => {
|
|||||||
var data = {
|
var data = {
|
||||||
'alert': 'alert'
|
'alert': 'alert'
|
||||||
};
|
};
|
||||||
var pushId = 1;
|
|
||||||
var timeStamp = 1454538822113;
|
var timeStamp = 1454538822113;
|
||||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
var timeStampISOStr = new Date(timeStamp).toISOString();
|
||||||
var expirationTime = 1454538822112;
|
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.priority).toEqual('normal');
|
||||||
expect(payload.timeToLive).toEqual(0);
|
expect(payload.timeToLive).toEqual(0);
|
||||||
var dataFromPayload = payload.data;
|
var dataFromPayload = payload.data;
|
||||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
|
||||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||||
expect(dataFromUser).toEqual(data);
|
expect(dataFromUser).toEqual(data);
|
||||||
done();
|
done();
|
||||||
@@ -88,19 +82,17 @@ describe('GCM', () => {
|
|||||||
var data = {
|
var data = {
|
||||||
'alert': 'alert'
|
'alert': 'alert'
|
||||||
};
|
};
|
||||||
var pushId = 1;
|
|
||||||
var timeStamp = 1454538822113;
|
var timeStamp = 1454538822113;
|
||||||
var timeStampISOStr = new Date(timeStamp).toISOString();
|
var timeStampISOStr = new Date(timeStamp).toISOString();
|
||||||
var expirationTime = 2454538822113;
|
var expirationTime = 2454538822113;
|
||||||
|
|
||||||
var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime);
|
var payload = GCM.generateGCMPayload(data, timeStamp, expirationTime);
|
||||||
|
|
||||||
expect(payload.priority).toEqual('normal');
|
expect(payload.priority).toEqual('normal');
|
||||||
// Four week in second
|
// Four week in second
|
||||||
expect(payload.timeToLive).toEqual(4 * 7 * 24 * 60 * 60);
|
expect(payload.timeToLive).toEqual(4 * 7 * 24 * 60 * 60);
|
||||||
var dataFromPayload = payload.data;
|
var dataFromPayload = payload.data;
|
||||||
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
expect(dataFromPayload.time).toEqual(timeStampISOStr);
|
||||||
expect(dataFromPayload['push_id']).toEqual(pushId);
|
|
||||||
var dataFromUser = JSON.parse(dataFromPayload.data);
|
var dataFromUser = JSON.parse(dataFromPayload.data);
|
||||||
expect(dataFromUser).toEqual(data);
|
expect(dataFromUser).toEqual(data);
|
||||||
done();
|
done();
|
||||||
@@ -139,6 +131,46 @@ describe('GCM', () => {
|
|||||||
done();
|
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) => {
|
it('can slice devices', (done) => {
|
||||||
// Mock devices
|
// Mock devices
|
||||||
var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)];
|
var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)];
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
|
'use strict';
|
||||||
describe('Parse.Push', () => {
|
describe('Parse.Push', () => {
|
||||||
it('should properly send push', (done) => {
|
it('should properly send push', (done) => {
|
||||||
var pushAdapter = {
|
var pushAdapter = {
|
||||||
send: function(body, installations) {
|
send: function(body, installations) {
|
||||||
var badge = body.data.badge;
|
var badge = body.data.badge;
|
||||||
installations.forEach((installation) => {
|
let promises = installations.map((installation) => {
|
||||||
if (installation.deviceType == "ios") {
|
if (installation.deviceType == "ios") {
|
||||||
expect(installation.badge).toEqual(badge);
|
expect(installation.badge).toEqual(badge);
|
||||||
expect(installation.originalBadge+1).toEqual(installation.badge);
|
expect(installation.originalBadge+1).toEqual(installation.badge);
|
||||||
} else {
|
} else {
|
||||||
expect(installation.badge).toBeUndefined();
|
expect(installation.badge).toBeUndefined();
|
||||||
}
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
err: null,
|
||||||
|
deviceType: installation.deviceType,
|
||||||
|
result: true
|
||||||
|
})
|
||||||
});
|
});
|
||||||
return Promise.resolve({
|
return Promise.all(promises)
|
||||||
body: body,
|
|
||||||
installations: installations
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
getValidPushTypes: function() {
|
getValidPushTypes: function() {
|
||||||
return ["ios", "android"];
|
return ["ios", "android"];
|
||||||
@@ -56,4 +59,4 @@ describe('Parse.Push', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1153,7 +1153,6 @@ describe('Parse.ACL', () => {
|
|||||||
var query = new Parse.Query("TestClassMasterACL");
|
var query = new Parse.Query("TestClassMasterACL");
|
||||||
return query.find();
|
return query.find();
|
||||||
}).then((results) => {
|
}).then((results) => {
|
||||||
console.log(JSON.stringify(results[0]));
|
|
||||||
ok(!results.length, 'Should not have returned object with secure ACL.');
|
ok(!results.length, 'Should not have returned object with secure ACL.');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,30 @@ var PushController = require('../src/Controllers/PushController').PushController
|
|||||||
|
|
||||||
var Config = require('../src/Config');
|
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', () => {
|
describe('PushController', () => {
|
||||||
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
|
||||||
@@ -105,9 +129,9 @@ describe('PushController', () => {
|
|||||||
}).toThrow();
|
}).toThrow();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('properly increment badges', (done) => {
|
it('properly increment badges', (done) => {
|
||||||
|
|
||||||
var payload = {data:{
|
var payload = {data:{
|
||||||
alert: "Hello World!",
|
alert: "Hello World!",
|
||||||
badge: "Increment",
|
badge: "Increment",
|
||||||
@@ -122,7 +146,7 @@ describe('PushController', () => {
|
|||||||
installation.set("deviceType", "ios");
|
installation.set("deviceType", "ios");
|
||||||
installations.push(installation);
|
installations.push(installation);
|
||||||
}
|
}
|
||||||
|
|
||||||
while(installations.length != 15) {
|
while(installations.length != 15) {
|
||||||
var installation = new Parse.Object("_Installation");
|
var installation = new Parse.Object("_Installation");
|
||||||
installation.set("installationId", "installation_"+installations.length);
|
installation.set("installationId", "installation_"+installations.length);
|
||||||
@@ -130,7 +154,7 @@ describe('PushController', () => {
|
|||||||
installation.set("deviceType", "android");
|
installation.set("deviceType", "android");
|
||||||
installations.push(installation);
|
installations.push(installation);
|
||||||
}
|
}
|
||||||
|
|
||||||
var pushAdapter = {
|
var pushAdapter = {
|
||||||
send: function(body, installations) {
|
send: function(body, installations) {
|
||||||
var badge = body.data.badge;
|
var badge = body.data.badge;
|
||||||
@@ -142,23 +166,20 @@ describe('PushController', () => {
|
|||||||
expect(installation.badge).toBeUndefined();
|
expect(installation.badge).toBeUndefined();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return Promise.resolve({
|
return successfulTransmissions(body, installations);
|
||||||
body: body,
|
|
||||||
installations: installations
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
getValidPushTypes: function() {
|
getValidPushTypes: function() {
|
||||||
return ["ios", "android"];
|
return ["ios", "android"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = new Config(Parse.applicationId);
|
var config = new Config(Parse.applicationId);
|
||||||
var auth = {
|
var auth = {
|
||||||
isMaster: true
|
isMaster: true
|
||||||
}
|
}
|
||||||
|
|
||||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
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);
|
return pushController.sendPush(payload, {}, config, auth);
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
done();
|
done();
|
||||||
@@ -167,11 +188,11 @@ describe('PushController', () => {
|
|||||||
fail("should not fail");
|
fail("should not fail");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('properly set badges to 1', (done) => {
|
it('properly set badges to 1', (done) => {
|
||||||
|
|
||||||
var payload = {data: {
|
var payload = {data: {
|
||||||
alert: "Hello World!",
|
alert: "Hello World!",
|
||||||
badge: 1,
|
badge: 1,
|
||||||
@@ -186,7 +207,7 @@ describe('PushController', () => {
|
|||||||
installation.set("deviceType", "ios");
|
installation.set("deviceType", "ios");
|
||||||
installations.push(installation);
|
installations.push(installation);
|
||||||
}
|
}
|
||||||
|
|
||||||
var pushAdapter = {
|
var pushAdapter = {
|
||||||
send: function(body, installations) {
|
send: function(body, installations) {
|
||||||
var badge = body.data.badge;
|
var badge = body.data.badge;
|
||||||
@@ -194,23 +215,20 @@ describe('PushController', () => {
|
|||||||
expect(installation.badge).toEqual(badge);
|
expect(installation.badge).toEqual(badge);
|
||||||
expect(1).toEqual(installation.badge);
|
expect(1).toEqual(installation.badge);
|
||||||
})
|
})
|
||||||
return Promise.resolve({
|
return successfulTransmissions(body, installations);
|
||||||
body: body,
|
|
||||||
installations: installations
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
getValidPushTypes: function() {
|
getValidPushTypes: function() {
|
||||||
return ["ios"];
|
return ["ios"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = new Config(Parse.applicationId);
|
var config = new Config(Parse.applicationId);
|
||||||
var auth = {
|
var auth = {
|
||||||
isMaster: true
|
isMaster: true
|
||||||
}
|
}
|
||||||
|
|
||||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
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);
|
return pushController.sendPush(payload, {}, config, auth);
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
done();
|
done();
|
||||||
@@ -219,29 +237,106 @@ describe('PushController', () => {
|
|||||||
fail("should not fail");
|
fail("should not fail");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support full RESTQuery for increment', (done) => {
|
it('properly creates _PushStatus', (done) => {
|
||||||
var payload = {data: {
|
|
||||||
|
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!",
|
alert: "Hello World!",
|
||||||
badge: 'Increment',
|
badge: 1,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
var pushAdapter = {
|
var pushAdapter = {
|
||||||
send: function(body, installations) {
|
send: function(body, installations) {
|
||||||
return Promise.resolve();
|
return successfulIOS(body, installations);
|
||||||
},
|
},
|
||||||
getValidPushTypes: function() {
|
getValidPushTypes: function() {
|
||||||
return ["ios"];
|
return ["ios"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = new Config(Parse.applicationId);
|
var config = new Config(Parse.applicationId);
|
||||||
var auth = {
|
var auth = {
|
||||||
isMaster: true
|
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 = {
|
let where = {
|
||||||
'deviceToken': {
|
'deviceToken': {
|
||||||
'$inQuery': {
|
'$inQuery': {
|
||||||
|
|||||||
33
src/APNS.js
33
src/APNS.js
@@ -66,6 +66,13 @@ function APNS(args) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
conn.on('transmitted', function(notification, device) {
|
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'));
|
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 coreData = data.data;
|
||||||
let expirationTime = data['expiration_time'];
|
let expirationTime = data['expiration_time'];
|
||||||
let notification = generateNotification(coreData, expirationTime);
|
let notification = generateNotification(coreData, expirationTime);
|
||||||
for (let device of devices) {
|
|
||||||
|
let promises = devices.map((device) => {
|
||||||
let qualifiedConnIndexs = chooseConns(this.conns, device);
|
let qualifiedConnIndexs = chooseConns(this.conns, device);
|
||||||
// We can not find a valid conn, just ignore this device
|
// We can not find a valid conn, just ignore this device
|
||||||
if (qualifiedConnIndexs.length == 0) {
|
if (qualifiedConnIndexs.length == 0) {
|
||||||
continue;
|
return Promise.resolve({
|
||||||
|
transmitted: false,
|
||||||
|
result: {error: 'No connection available'}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let conn = this.conns[qualifiedConnIndexs[0]];
|
let conn = this.conns[qualifiedConnIndexs[0]];
|
||||||
let apnDevice = new apn.Device(device.deviceToken);
|
let apnDevice = new apn.Device(device.deviceToken);
|
||||||
@@ -104,13 +115,15 @@ APNS.prototype.send = function(data, devices) {
|
|||||||
if (device.appIdentifier) {
|
if (device.appIdentifier) {
|
||||||
apnDevice.appIdentifier = device.appIdentifier;
|
apnDevice.appIdentifier = device.appIdentifier;
|
||||||
}
|
}
|
||||||
conn.pushNotification(notification, apnDevice);
|
return new Promise((resolve, reject) => {
|
||||||
}
|
apnDevice.callback = resolve;
|
||||||
return Parse.Promise.as();
|
conn.pushNotification(notification, apnDevice);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Parse.Promise.when(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTransmissionError(conns, errCode, notification, apnDevice) {
|
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,
|
// This means the error notification is not in the cache anymore or the recepient is missing,
|
||||||
// we just ignore this case
|
// we just ignore this case
|
||||||
if (!notification || !apnDevice) {
|
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
|
// There is no more available conns, we give up in this case
|
||||||
if (newConnIndex < 0 || newConnIndex >= conns.length) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ var deepcopy = require('deepcopy');
|
|||||||
import PushAdapter from './PushAdapter';
|
import PushAdapter from './PushAdapter';
|
||||||
|
|
||||||
export class OneSignalPushAdapter extends PushAdapter {
|
export class OneSignalPushAdapter extends PushAdapter {
|
||||||
|
|
||||||
constructor(pushConfig = {}) {
|
constructor(pushConfig = {}) {
|
||||||
super(pushConfig);
|
super(pushConfig);
|
||||||
this.https = require('https');
|
this.https = require('https');
|
||||||
|
|
||||||
this.validPushTypes = ['ios', 'android'];
|
this.validPushTypes = ['ios', 'android'];
|
||||||
this.senderMap = {};
|
this.senderMap = {};
|
||||||
this.OneSignalConfig = {};
|
this.OneSignalConfig = {};
|
||||||
@@ -24,13 +24,12 @@ export class OneSignalPushAdapter extends PushAdapter {
|
|||||||
}
|
}
|
||||||
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
|
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
|
||||||
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
|
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
|
||||||
|
|
||||||
this.senderMap['ios'] = this.sendToAPNS.bind(this);
|
this.senderMap['ios'] = this.sendToAPNS.bind(this);
|
||||||
this.senderMap['android'] = this.sendToGCM.bind(this);
|
this.senderMap['android'] = this.sendToGCM.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
send(data, installations) {
|
send(data, installations) {
|
||||||
console.log("Sending notification to "+installations.length+" devices.")
|
|
||||||
let deviceMap = classifyInstallations(installations, this.validPushTypes);
|
let deviceMap = classifyInstallations(installations, this.validPushTypes);
|
||||||
|
|
||||||
let sendPromises = [];
|
let sendPromises = [];
|
||||||
@@ -48,15 +47,15 @@ export class OneSignalPushAdapter extends PushAdapter {
|
|||||||
}
|
}
|
||||||
return Parse.Promise.when(sendPromises);
|
return Parse.Promise.when(sendPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
static classifyInstallations(installations, validTypes) {
|
static classifyInstallations(installations, validTypes) {
|
||||||
return classifyInstallations(installations, validTypes)
|
return classifyInstallations(installations, validTypes)
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidPushTypes() {
|
getValidPushTypes() {
|
||||||
return this.validPushTypes;
|
return this.validPushTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToAPNS(data,tokens) {
|
sendToAPNS(data,tokens) {
|
||||||
|
|
||||||
data= deepcopy(data['data']);
|
data= deepcopy(data['data']);
|
||||||
@@ -117,19 +116,19 @@ export class OneSignalPushAdapter extends PushAdapter {
|
|||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToGCM(data,tokens) {
|
sendToGCM(data,tokens) {
|
||||||
data= deepcopy(data['data']);
|
data= deepcopy(data['data']);
|
||||||
|
|
||||||
var post = {};
|
var post = {};
|
||||||
|
|
||||||
if(data['alert']) {
|
if(data['alert']) {
|
||||||
post['contents'] = {en: data['alert']};
|
post['contents'] = {en: data['alert']};
|
||||||
delete data['alert'];
|
delete data['alert'];
|
||||||
}
|
}
|
||||||
if(data['title']) {
|
if(data['title']) {
|
||||||
post['title'] = {en: data['title']};
|
post['title'] = {en: data['title']};
|
||||||
delete data['title'];
|
delete data['title'];
|
||||||
}
|
}
|
||||||
if(data['uri']) {
|
if(data['uri']) {
|
||||||
post['url'] = data['uri'];
|
post['url'] = data['uri'];
|
||||||
@@ -155,7 +154,7 @@ export class OneSignalPushAdapter extends PushAdapter {
|
|||||||
}
|
}
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
|
|
||||||
this.sendNext = function() {
|
this.sendNext = function() {
|
||||||
post['include_android_reg_ids'] = [];
|
post['include_android_reg_ids'] = [];
|
||||||
tokens.slice(offset,offset+chunk).forEach(function(i) {
|
tokens.slice(offset,offset+chunk).forEach(function(i) {
|
||||||
post['include_android_reg_ids'].push(i['deviceToken'])
|
post['include_android_reg_ids'].push(i['deviceToken'])
|
||||||
@@ -168,7 +167,7 @@ export class OneSignalPushAdapter extends PushAdapter {
|
|||||||
this.sendNext();
|
this.sendNext();
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToOneSignal(data, cb) {
|
sendToOneSignal(data, cb) {
|
||||||
let headers = {
|
let headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -188,7 +187,7 @@ export class OneSignalPushAdapter extends PushAdapter {
|
|||||||
cb(true);
|
cb(true);
|
||||||
} else {
|
} else {
|
||||||
console.log('OneSignal Error');
|
console.log('OneSignal Error');
|
||||||
res.on('data', function(chunk) {
|
res.on('data', function(chunk) {
|
||||||
console.log(chunk.toString())
|
console.log(chunk.toString())
|
||||||
});
|
});
|
||||||
cb(false)
|
cb(false)
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import PushAdapter from './PushAdapter';
|
|||||||
import { classifyInstallations } from './PushAdapterUtils';
|
import { classifyInstallations } from './PushAdapterUtils';
|
||||||
|
|
||||||
export class ParsePushAdapter extends PushAdapter {
|
export class ParsePushAdapter extends PushAdapter {
|
||||||
|
|
||||||
|
supportsPushTracking = true;
|
||||||
|
|
||||||
constructor(pushConfig = {}) {
|
constructor(pushConfig = {}) {
|
||||||
super(pushConfig);
|
super(pushConfig);
|
||||||
this.validPushTypes = ['ios', 'android'];
|
this.validPushTypes = ['ios', 'android'];
|
||||||
@@ -19,7 +22,7 @@ export class ParsePushAdapter extends PushAdapter {
|
|||||||
immediatePush: true
|
immediatePush: true
|
||||||
};
|
};
|
||||||
let pushTypes = Object.keys(pushConfig);
|
let pushTypes = Object.keys(pushConfig);
|
||||||
|
|
||||||
for (let pushType of pushTypes) {
|
for (let pushType of pushTypes) {
|
||||||
if (this.validPushTypes.indexOf(pushType) < 0) {
|
if (this.validPushTypes.indexOf(pushType) < 0) {
|
||||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||||
@@ -35,7 +38,7 @@ export class ParsePushAdapter extends PushAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidPushTypes() {
|
getValidPushTypes() {
|
||||||
return this.validPushTypes;
|
return this.validPushTypes;
|
||||||
}
|
}
|
||||||
@@ -43,18 +46,21 @@ export class ParsePushAdapter extends PushAdapter {
|
|||||||
static classifyInstallations(installations, validTypes) {
|
static classifyInstallations(installations, validTypes) {
|
||||||
return classifyInstallations(installations, validTypes)
|
return classifyInstallations(installations, validTypes)
|
||||||
}
|
}
|
||||||
|
|
||||||
send(data, installations) {
|
send(data, installations) {
|
||||||
let deviceMap = classifyInstallations(installations, this.validPushTypes);
|
let deviceMap = classifyInstallations(installations, this.validPushTypes);
|
||||||
let sendPromises = [];
|
let sendPromises = [];
|
||||||
for (let pushType in deviceMap) {
|
for (let pushType in deviceMap) {
|
||||||
let sender = this.senderMap[pushType];
|
let sender = this.senderMap[pushType];
|
||||||
if (!sender) {
|
if (!sender) {
|
||||||
console.log('Can not find sender for push type %s, %j', pushType, data);
|
sendPromises.push(Promise.resolve({
|
||||||
continue;
|
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);
|
return Parse.Promise.when(sendPromises);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
//
|
//
|
||||||
// Adapter classes must implement the following functions:
|
// Adapter classes must implement the following functions:
|
||||||
// * getValidPushTypes()
|
// * getValidPushTypes()
|
||||||
// * send(devices, installations)
|
// * send(devices, installations, pushStatus)
|
||||||
//
|
//
|
||||||
// Default is ParsePushAdapter, which uses GCM for
|
// Default is ParsePushAdapter, which uses GCM for
|
||||||
// android push and APNS for ios push.
|
// android push and APNS for ios push.
|
||||||
|
|
||||||
export class PushAdapter {
|
export class PushAdapter {
|
||||||
send(devices, installations) { }
|
send(devices, installations, pushStatus) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an array of valid push types.
|
* Get an array of valid push types.
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ export function classifyInstallations(installations, validPushTypes) {
|
|||||||
deviceToken: installation.deviceToken,
|
deviceToken: installation.deviceToken,
|
||||||
appIdentifier: installation.appIdentifier
|
appIdentifier: installation.appIdentifier
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
console.log('Unknown push type from installation %j', installation);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deviceMap;
|
return deviceMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { PushAdapter } from '../Adapters/Push/PushAdapter';
|
|||||||
import deepcopy from 'deepcopy';
|
import deepcopy from 'deepcopy';
|
||||||
import features from '../features';
|
import features from '../features';
|
||||||
import RestQuery from '../RestQuery';
|
import RestQuery from '../RestQuery';
|
||||||
|
import pushStatusHandler from '../pushStatusHandler';
|
||||||
|
|
||||||
const FEATURE_NAME = 'push';
|
const FEATURE_NAME = 'push';
|
||||||
const UNSUPPORTED_BADGE_KEY = "unsupported";
|
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;
|
var pushAdapter = this.adapter;
|
||||||
if (!pushAdapter) {
|
if (!pushAdapter) {
|
||||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||||
@@ -65,7 +66,7 @@ export class PushController extends AdaptableController {
|
|||||||
}
|
}
|
||||||
let updateWhere = deepcopy(where);
|
let updateWhere = deepcopy(where);
|
||||||
|
|
||||||
badgeUpdate = () => {
|
badgeUpdate = () => {
|
||||||
let badgeQuery = new RestQuery(config, auth, '_Installation', updateWhere);
|
let badgeQuery = new RestQuery(config, auth, '_Installation', updateWhere);
|
||||||
return badgeQuery.buildRestWhere().then(() => {
|
return badgeQuery.buildRestWhere().then(() => {
|
||||||
let restWhere = deepcopy(badgeQuery.restWhere);
|
let restWhere = deepcopy(badgeQuery.restWhere);
|
||||||
@@ -81,38 +82,58 @@ export class PushController extends AdaptableController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let pushStatus = pushStatusHandler(config);
|
||||||
return badgeUpdate().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
|
return pushStatus.setInitial(body, where);
|
||||||
|
}).then(() => {
|
||||||
|
return badgeUpdate();
|
||||||
|
}).then(() => {
|
||||||
return rest.find(config, auth, '_Installation', where);
|
return rest.find(config, auth, '_Installation', where);
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
if (body.data && body.data.badge && body.data.badge == "Increment") {
|
pushStatus.setRunning();
|
||||||
// Collect the badges to reduce the # of calls
|
return this.sendToAdapter(body, response.results, pushStatus, config);
|
||||||
let badgeInstallationsMap = response.results.reduce((map, installation) => {
|
}).then((results) => {
|
||||||
let badge = installation.badge;
|
return pushStatus.complete(results);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
* Get expiration time from the request body.
|
||||||
* @param {Object} request A request object
|
* @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
|
* @returns {Object} A promise which is resolved after we get results from gcm
|
||||||
*/
|
*/
|
||||||
GCM.prototype.send = function(data, devices) {
|
GCM.prototype.send = function(data, devices) {
|
||||||
let pushId = cryptoUtils.newObjectId();
|
// Make a new array
|
||||||
let timeStamp = Date.now();
|
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;
|
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
|
||||||
@@ -31,33 +52,51 @@ GCM.prototype.send = function(data, devices) {
|
|||||||
expirationTime = data['expiration_time'];
|
expirationTime = data['expiration_time'];
|
||||||
}
|
}
|
||||||
// Generate gcm payload
|
// Generate gcm payload
|
||||||
let gcmPayload = generateGCMPayload(data.data, pushId, timeStamp, expirationTime);
|
let gcmPayload = generateGCMPayload(data.data, timestamp, expirationTime);
|
||||||
// Make and send gcm request
|
// Make and send gcm request
|
||||||
let message = new gcm.Message(gcmPayload);
|
let message = new gcm.Message(gcmPayload);
|
||||||
|
|
||||||
let sendPromises = [];
|
// Build a device map
|
||||||
// For android, we can only have 1000 recepients per send, so we need to slice devices to
|
let devicesMap = devices.reduce((memo, device) => {
|
||||||
// chunk if necessary
|
memo[device.deviceToken] = device;
|
||||||
let chunkDevices = sliceDevices(devices, GCMRegistrationTokensMax);
|
return memo;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
* @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
|
||||||
*/
|
*/
|
||||||
function generateGCMPayload(coreData, pushId, timeStamp, expirationTime) {
|
function generateGCMPayload(coreData, timeStamp, expirationTime) {
|
||||||
let payloadData = {
|
let payloadData = {
|
||||||
'time': new Date(timeStamp).toISOString(),
|
'time': new Date(timeStamp).toISOString(),
|
||||||
'push_id': pushId,
|
|
||||||
'data': JSON.stringify(coreData)
|
'data': JSON.stringify(coreData)
|
||||||
}
|
}
|
||||||
let payload = {
|
let payload = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// An object that encapsulates everything we need to run a 'find'
|
// An object that encapsulates everything we need to run a 'find'
|
||||||
// operation, encoded in the REST API format.
|
// operation, encoded in the REST API format.
|
||||||
|
|
||||||
|
var Schema = require('./Schema');
|
||||||
var Parse = require('parse/node').Parse;
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
import { default as FilesController } from './Controllers/FilesController';
|
import { default as FilesController } from './Controllers/FilesController';
|
||||||
@@ -170,7 +171,7 @@ RestQuery.prototype.redirectClassNameForKey = function() {
|
|||||||
|
|
||||||
// Validates this operation against the allowClientClassCreation config.
|
// Validates this operation against the allowClientClassCreation config.
|
||||||
RestQuery.prototype.validateClientClassCreation = function() {
|
RestQuery.prototype.validateClientClassCreation = function() {
|
||||||
let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product'];
|
let sysClass = Schema.systemClasses;
|
||||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||||
&& sysClass.indexOf(this.className) === -1) {
|
&& sysClass.indexOf(this.className) === -1) {
|
||||||
return this.config.database.collectionExists(this.className).then((hasClass) => {
|
return this.config.database.collectionExists(this.className).then((hasClass) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// This could be either a "create" or an "update".
|
// This could be either a "create" or an "update".
|
||||||
|
|
||||||
import cache from './cache';
|
import cache from './cache';
|
||||||
|
var Schema = require('./Schema');
|
||||||
var deepcopy = require('deepcopy');
|
var deepcopy = require('deepcopy');
|
||||||
|
|
||||||
var Auth = require('./Auth');
|
var Auth = require('./Auth');
|
||||||
@@ -108,7 +109,7 @@ RestWrite.prototype.getUserAndRoleACL = function() {
|
|||||||
|
|
||||||
// Validates this operation against the allowClientClassCreation config.
|
// Validates this operation against the allowClientClassCreation config.
|
||||||
RestWrite.prototype.validateClientClassCreation = function() {
|
RestWrite.prototype.validateClientClassCreation = function() {
|
||||||
let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product'];
|
let sysClass = Schema.systemClasses;
|
||||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||||
&& sysClass.indexOf(this.className) === -1) {
|
&& sysClass.indexOf(this.className) === -1) {
|
||||||
return this.config.database.collectionExists(this.className).then((hasClass) => {
|
return this.config.database.collectionExists(this.className).then((hasClass) => {
|
||||||
|
|||||||
@@ -67,7 +67,22 @@ var defaultColumns = {
|
|||||||
"icon": {type:'File'},
|
"icon": {type:'File'},
|
||||||
"order": {type:'Number'},
|
"order": {type:'Number'},
|
||||||
"title": {type:'String'},
|
"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"]
|
_Role: ["name", "ACL"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const systemClasses = ['_User', '_Installation', '_Role', '_Session', '_Product'];
|
||||||
|
|
||||||
// 10 alpha numberic chars + uppercase
|
// 10 alpha numberic chars + uppercase
|
||||||
const userIdRegex = /^[a-zA-Z0-9]{10}$/;
|
const userIdRegex = /^[a-zA-Z0-9]{10}$/;
|
||||||
// Anything that start with role
|
// Anything that start with role
|
||||||
@@ -127,13 +144,8 @@ function validateCLP(perms) {
|
|||||||
var joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
|
var joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
|
||||||
var classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
|
var classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
|
||||||
function classNameIsValid(className) {
|
function classNameIsValid(className) {
|
||||||
return (
|
return (systemClasses.indexOf(className) > -1 ||
|
||||||
className === '_User' ||
|
|
||||||
className === '_Installation' ||
|
|
||||||
className === '_Session' ||
|
|
||||||
className === '_SCHEMA' || //TODO: remove this, as _SCHEMA is not a valid class name for storing Parse Objects.
|
className === '_SCHEMA' || //TODO: remove this, as _SCHEMA is not a valid class name for storing Parse Objects.
|
||||||
className === '_Role' ||
|
|
||||||
className === '_Product' ||
|
|
||||||
joinClassRegex.test(className) ||
|
joinClassRegex.test(className) ||
|
||||||
//Class names have the same constraints as field names, but also allow the previous additional names.
|
//Class names have the same constraints as field names, but also allow the previous additional names.
|
||||||
fieldNameIsValid(className)
|
fieldNameIsValid(className)
|
||||||
@@ -284,7 +296,7 @@ class Schema {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateClass(className, submittedFields, classLevelPermissions, database) {
|
updateClass(className, submittedFields, classLevelPermissions, database) {
|
||||||
if (!this.data[className]) {
|
if (!this.data[className]) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
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.`);
|
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let newSchema = buildMergedSchemaObject(existingFields, submittedFields);
|
let newSchema = buildMergedSchemaObject(existingFields, submittedFields);
|
||||||
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(newSchema, className, classLevelPermissions);
|
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(newSchema, className, classLevelPermissions);
|
||||||
if (!mongoObject.result) {
|
if (!mongoObject.result) {
|
||||||
@@ -327,7 +339,7 @@ class Schema {
|
|||||||
});
|
});
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return this.setPermissions(className, classLevelPermissions)
|
return this.setPermissions(className, classLevelPermissions)
|
||||||
})
|
})
|
||||||
.then(() => { return mongoSchemaToSchemaAPIResponse(mongoObject.result) });
|
.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.',
|
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
validateCLP(classLevelPermissions);
|
validateCLP(classLevelPermissions);
|
||||||
if (typeof classLevelPermissions !== 'undefined') {
|
if (typeof classLevelPermissions !== 'undefined') {
|
||||||
mongoObject._metadata = mongoObject._metadata || {};
|
mongoObject._metadata = mongoObject._metadata || {};
|
||||||
@@ -886,11 +898,11 @@ function mongoSchemaToSchemaAPIResponse(schema) {
|
|||||||
className: schema._id,
|
className: schema._id,
|
||||||
fields: mongoSchemaAPIResponseFields(schema),
|
fields: mongoSchemaAPIResponseFields(schema),
|
||||||
};
|
};
|
||||||
|
|
||||||
let classLevelPermissions = DefaultClassLevelPermissions;
|
let classLevelPermissions = DefaultClassLevelPermissions;
|
||||||
if (schema._metadata && schema._metadata.class_permissions) {
|
if (schema._metadata && schema._metadata.class_permissions) {
|
||||||
classLevelPermissions = Object.assign(classLevelPermissions, schema._metadata.class_permissions);
|
classLevelPermissions = Object.assign(classLevelPermissions, schema._metadata.class_permissions);
|
||||||
}
|
}
|
||||||
result.classLevelPermissions = classLevelPermissions;
|
result.classLevelPermissions = classLevelPermissions;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -903,4 +915,5 @@ module.exports = {
|
|||||||
buildMergedSchemaObject: buildMergedSchemaObject,
|
buildMergedSchemaObject: buildMergedSchemaObject,
|
||||||
mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType,
|
mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType,
|
||||||
mongoSchemaToSchemaAPIResponse,
|
mongoSchemaToSchemaAPIResponse,
|
||||||
|
systemClasses,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes, createHash } from 'crypto';
|
||||||
|
|
||||||
// Returns a new random hex string of the given even size.
|
// Returns a new random hex string of the given even size.
|
||||||
export function randomHexString(size: number): string {
|
export function randomHexString(size: number): string {
|
||||||
@@ -44,3 +44,7 @@ export function newObjectId(): string {
|
|||||||
export function newToken(): string {
|
export function newToken(): string {
|
||||||
return randomHexString(32);
|
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