ParsePushAdapter is a package
This commit is contained in:
227
src/APNS.js
227
src/APNS.js
@@ -1,227 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
// TODO: apn does not support the new HTTP/2 protocal. It is fine to use it in V1,
|
||||
// but probably we will replace it in the future.
|
||||
const apn = require('apn');
|
||||
|
||||
/**
|
||||
* Create a new connection to the APN service.
|
||||
* @constructor
|
||||
* @param {Object|Array} args An argument or a list of arguments to config APNS connection
|
||||
* @param {String} args.cert The filename of the connection certificate to load from disk
|
||||
* @param {String} args.key The filename of the connection key to load from disk
|
||||
* @param {String} args.pfx The filename for private key, certificate and CA certs in PFX or PKCS12 format, it will overwrite cert and key
|
||||
* @param {String} args.passphrase The passphrase for the connection key, if required
|
||||
* @param {String} args.bundleId The bundleId for cert
|
||||
* @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox
|
||||
*/
|
||||
function APNS(args) {
|
||||
// Since for ios, there maybe multiple cert/key pairs,
|
||||
// typePushConfig can be an array.
|
||||
let apnsArgsList = [];
|
||||
if (Array.isArray(args)) {
|
||||
apnsArgsList = apnsArgsList.concat(args);
|
||||
} else if (typeof args === 'object') {
|
||||
apnsArgsList.push(args);
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'APNS Configuration is invalid');
|
||||
}
|
||||
|
||||
this.conns = [];
|
||||
for (let apnsArgs of apnsArgsList) {
|
||||
let conn = new apn.Connection(apnsArgs);
|
||||
if (!apnsArgs.bundleId) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'BundleId is mssing for %j', apnsArgs);
|
||||
}
|
||||
conn.bundleId = apnsArgs.bundleId;
|
||||
// Set the priority of the conns, prod cert has higher priority
|
||||
if (apnsArgs.production) {
|
||||
conn.priority = 0;
|
||||
} else {
|
||||
conn.priority = 1;
|
||||
}
|
||||
|
||||
// Set apns client callbacks
|
||||
conn.on('connected', () => {
|
||||
console.log('APNS Connection %d Connected', conn.index);
|
||||
});
|
||||
|
||||
conn.on('transmissionError', (errCode, notification, apnDevice) => {
|
||||
handleTransmissionError(this.conns, errCode, notification, apnDevice);
|
||||
});
|
||||
|
||||
conn.on('timeout', () => {
|
||||
console.log('APNS Connection %d Timeout', conn.index);
|
||||
});
|
||||
|
||||
conn.on('disconnected', () => {
|
||||
console.log('APNS Connection %d Disconnected', conn.index);
|
||||
});
|
||||
|
||||
conn.on('socketError', () => {
|
||||
console.log('APNS Connection %d Socket Error', conn.index);
|
||||
});
|
||||
|
||||
conn.on('transmitted', function(notification, device) {
|
||||
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'));
|
||||
});
|
||||
|
||||
this.conns.push(conn);
|
||||
}
|
||||
// Sort the conn based on priority ascending, high pri first
|
||||
this.conns.sort((s1, s2) => {
|
||||
return s1.priority - s2.priority;
|
||||
});
|
||||
// Set index of conns
|
||||
for (let index = 0; index < this.conns.length; index++) {
|
||||
this.conns[index].index = index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send apns request.
|
||||
* @param {Object} data The data we need to send, the format is the same with api request body
|
||||
* @param {Array} devices A array of devices
|
||||
* @returns {Object} A promise which is resolved immediately
|
||||
*/
|
||||
APNS.prototype.send = function(data, devices) {
|
||||
let coreData = data.data;
|
||||
let expirationTime = data['expiration_time'];
|
||||
let notification = generateNotification(coreData, expirationTime);
|
||||
|
||||
let promises = devices.map((device) => {
|
||||
let qualifiedConnIndexs = chooseConns(this.conns, device);
|
||||
// We can not find a valid conn, just ignore this device
|
||||
if (qualifiedConnIndexs.length == 0) {
|
||||
return Promise.resolve({
|
||||
transmitted: false,
|
||||
result: {error: 'No connection available'}
|
||||
});
|
||||
}
|
||||
let conn = this.conns[qualifiedConnIndexs[0]];
|
||||
let apnDevice = new apn.Device(device.deviceToken);
|
||||
apnDevice.connIndex = qualifiedConnIndexs[0];
|
||||
// Add additional appIdentifier info to apn device instance
|
||||
if (device.appIdentifier) {
|
||||
apnDevice.appIdentifier = device.appIdentifier;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
apnDevice.callback = resolve;
|
||||
conn.pushNotification(notification, apnDevice);
|
||||
});
|
||||
});
|
||||
return Parse.Promise.when(promises);
|
||||
}
|
||||
|
||||
function handleTransmissionError(conns, errCode, notification, apnDevice) {
|
||||
// This means the error notification is not in the cache anymore or the recepient is missing,
|
||||
// we just ignore this case
|
||||
if (!notification || !apnDevice) {
|
||||
return
|
||||
}
|
||||
|
||||
// If currentConn can not send the push notification, we try to use the next available conn.
|
||||
// Since conns is sorted by priority, the next conn means the next low pri conn.
|
||||
// If there is no conn available, we give up on sending the notification to that device.
|
||||
let qualifiedConnIndexs = chooseConns(conns, apnDevice);
|
||||
let currentConnIndex = apnDevice.connIndex;
|
||||
|
||||
let newConnIndex = -1;
|
||||
// Find the next element of currentConnIndex in qualifiedConnIndexs
|
||||
for (let index = 0; index < qualifiedConnIndexs.length - 1; index++) {
|
||||
if (qualifiedConnIndexs[index] === currentConnIndex) {
|
||||
newConnIndex = qualifiedConnIndexs[index + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// There is no more available conns, we give up in this case
|
||||
if (newConnIndex < 0 || newConnIndex >= conns.length) {
|
||||
if (apnDevice.callback) {
|
||||
apnDevice.callback({
|
||||
response: {error: `APNS can not find vaild connection for ${apnDevice.token}`, code: errCode},
|
||||
status: errCode,
|
||||
transmitted: false
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let newConn = conns[newConnIndex];
|
||||
// Update device conn info
|
||||
apnDevice.connIndex = newConnIndex;
|
||||
// Use the new conn to send the notification
|
||||
newConn.pushNotification(notification, apnDevice);
|
||||
}
|
||||
|
||||
function chooseConns(conns, device) {
|
||||
// If device does not have appIdentifier, all conns maybe proper connections.
|
||||
// Otherwise we try to match the appIdentifier with bundleId
|
||||
let qualifiedConns = [];
|
||||
for (let index = 0; index < conns.length; index++) {
|
||||
let conn = conns[index];
|
||||
// If the device we need to send to does not have
|
||||
// appIdentifier, any conn could be a qualified connection
|
||||
if (!device.appIdentifier || device.appIdentifier === '') {
|
||||
qualifiedConns.push(index);
|
||||
continue;
|
||||
}
|
||||
if (device.appIdentifier === conn.bundleId) {
|
||||
qualifiedConns.push(index);
|
||||
}
|
||||
}
|
||||
return qualifiedConns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the apns notification from the data we get from api request.
|
||||
* @param {Object} coreData The data field under api request body
|
||||
* @returns {Object} A apns notification
|
||||
*/
|
||||
function generateNotification(coreData, expirationTime) {
|
||||
let notification = new apn.notification();
|
||||
let payload = {};
|
||||
for (let key in coreData) {
|
||||
switch (key) {
|
||||
case 'alert':
|
||||
notification.setAlertText(coreData.alert);
|
||||
break;
|
||||
case 'badge':
|
||||
notification.badge = coreData.badge;
|
||||
break;
|
||||
case 'sound':
|
||||
notification.sound = coreData.sound;
|
||||
break;
|
||||
case 'content-available':
|
||||
notification.setNewsstandAvailable(true);
|
||||
let isAvailable = coreData['content-available'] === 1;
|
||||
notification.setContentAvailable(isAvailable);
|
||||
break;
|
||||
case 'category':
|
||||
notification.category = coreData.category;
|
||||
break;
|
||||
default:
|
||||
payload[key] = coreData[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
notification.payload = payload;
|
||||
notification.expiry = expirationTime;
|
||||
return notification;
|
||||
}
|
||||
|
||||
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||
APNS.generateNotification = generateNotification;
|
||||
APNS.chooseConns = chooseConns;
|
||||
APNS.handleTransmissionError = handleTransmissionError;
|
||||
}
|
||||
module.exports = APNS;
|
||||
@@ -3,7 +3,8 @@
|
||||
// PushAdapter, it uses GCM for android push and APNS
|
||||
// for ios push.
|
||||
|
||||
import { classifyInstallations } from './PushAdapterUtils';
|
||||
import { utils } from 'parse-server-push-adapter';
|
||||
import ParsePushAdapter from 'parse-server-push-adapter';
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
var deepcopy = require('deepcopy');
|
||||
@@ -30,7 +31,7 @@ export class OneSignalPushAdapter extends PushAdapter {
|
||||
}
|
||||
|
||||
send(data, installations) {
|
||||
let deviceMap = classifyInstallations(installations, this.validPushTypes);
|
||||
let deviceMap = utils.classifyInstallations(installations, this.validPushTypes);
|
||||
|
||||
let sendPromises = [];
|
||||
for (let pushType in deviceMap) {
|
||||
@@ -49,7 +50,7 @@ export class OneSignalPushAdapter extends PushAdapter {
|
||||
}
|
||||
|
||||
static classifyInstallations(installations, validTypes) {
|
||||
return classifyInstallations(installations, validTypes)
|
||||
return utils.classifyInstallations(installations, validTypes)
|
||||
}
|
||||
|
||||
getValidPushTypes() {
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
"use strict";
|
||||
// ParsePushAdapter is the default implementation of
|
||||
// PushAdapter, it uses GCM for android push and APNS
|
||||
// for ios push.
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
const GCM = require('../../GCM');
|
||||
const APNS = require('../../APNS');
|
||||
import PushAdapter from './PushAdapter';
|
||||
import { classifyInstallations } from './PushAdapterUtils';
|
||||
|
||||
export class ParsePushAdapter extends PushAdapter {
|
||||
|
||||
supportsPushTracking = true;
|
||||
|
||||
constructor(pushConfig = {}) {
|
||||
super(pushConfig);
|
||||
this.validPushTypes = ['ios', 'android'];
|
||||
this.senderMap = {};
|
||||
// used in PushController for Dashboard Features
|
||||
this.feature = {
|
||||
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,
|
||||
'Push to ' + pushTypes + ' is not supported');
|
||||
}
|
||||
switch (pushType) {
|
||||
case 'ios':
|
||||
this.senderMap[pushType] = new APNS(pushConfig[pushType]);
|
||||
break;
|
||||
case 'android':
|
||||
this.senderMap[pushType] = new GCM(pushConfig[pushType]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValidPushTypes() {
|
||||
return this.validPushTypes;
|
||||
}
|
||||
|
||||
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) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
return Parse.Promise.when(sendPromises);
|
||||
}
|
||||
}
|
||||
|
||||
export default ParsePushAdapter;
|
||||
module.exports = ParsePushAdapter;
|
||||
@@ -1,27 +0,0 @@
|
||||
/**g
|
||||
* Classify the device token of installations based on its device type.
|
||||
* @param {Object} installations An array of installations
|
||||
* @param {Array} validPushTypes An array of valid push types(string)
|
||||
* @returns {Object} A map whose key is device type and value is an array of device
|
||||
*/
|
||||
export function classifyInstallations(installations, validPushTypes) {
|
||||
// Init deviceTokenMap, create a empty array for each valid pushType
|
||||
let deviceMap = {};
|
||||
for (let validPushType of validPushTypes) {
|
||||
deviceMap[validPushType] = [];
|
||||
}
|
||||
for (let installation of installations) {
|
||||
// No deviceToken, ignore
|
||||
if (!installation.deviceToken) {
|
||||
continue;
|
||||
}
|
||||
let pushType = installation.deviceType;
|
||||
if (deviceMap[pushType]) {
|
||||
deviceMap[pushType].push({
|
||||
deviceToken: installation.deviceToken,
|
||||
appIdentifier: installation.appIdentifier
|
||||
});
|
||||
}
|
||||
}
|
||||
return deviceMap;
|
||||
}
|
||||
154
src/GCM.js
154
src/GCM.js
@@ -1,154 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
const gcm = require('node-gcm');
|
||||
const cryptoUtils = require('./cryptoUtils');
|
||||
|
||||
const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
|
||||
const GCMRegistrationTokensMax = 1000;
|
||||
|
||||
function GCM(args) {
|
||||
if (typeof args !== 'object' || !args.apiKey) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'GCM Configuration is invalid');
|
||||
}
|
||||
this.sender = new gcm.Sender(args.apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send gcm request.
|
||||
* @param {Object} data The data we need to send, the format is the same with api request body
|
||||
* @param {Array} devices A array of devices
|
||||
* @returns {Object} A promise which is resolved after we get results from gcm
|
||||
*/
|
||||
GCM.prototype.send = function(data, devices) {
|
||||
let pushId = cryptoUtils.newObjectId();
|
||||
// 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
|
||||
if (data['expiration_time']) {
|
||||
expirationTime = data['expiration_time'];
|
||||
}
|
||||
// Generate gcm payload
|
||||
// PushId is not a formal field of GCM, but Parse Android SDK uses this field to deduplicate push notifications
|
||||
let gcmPayload = generateGCMPayload(data.data, pushId, timestamp, expirationTime);
|
||||
// Make and send gcm request
|
||||
let message = new gcm.Message(gcmPayload);
|
||||
|
||||
// Build a device map
|
||||
let devicesMap = devices.reduce((memo, device) => {
|
||||
memo[device.deviceToken] = device;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the gcm payload from the data we get from api request.
|
||||
* @param {Object} coreData The data field under api request body
|
||||
* @param {String} pushId A random string
|
||||
* @param {Number} timeStamp A number whose format is the Unix Epoch
|
||||
* @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) {
|
||||
let payloadData = {
|
||||
'time': new Date(timeStamp).toISOString(),
|
||||
'push_id': pushId,
|
||||
'data': JSON.stringify(coreData)
|
||||
}
|
||||
let payload = {
|
||||
priority: 'normal',
|
||||
data: payloadData
|
||||
};
|
||||
if (expirationTime) {
|
||||
// The timeStamp and expiration is in milliseconds but gcm requires second
|
||||
let timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
|
||||
if (timeToLive < 0) {
|
||||
timeToLive = 0;
|
||||
}
|
||||
if (timeToLive >= GCMTimeToLiveMax) {
|
||||
timeToLive = GCMTimeToLiveMax;
|
||||
}
|
||||
payload.timeToLive = timeToLive;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice a list of devices to several list of devices with fixed chunk size.
|
||||
* @param {Array} devices An array of devices
|
||||
* @param {Number} chunkSize The size of the a chunk
|
||||
* @returns {Array} An array which contaisn several arries of devices with fixed chunk size
|
||||
*/
|
||||
function sliceDevices(devices, chunkSize) {
|
||||
let chunkDevices = [];
|
||||
while (devices.length > 0) {
|
||||
chunkDevices.push(devices.splice(0, chunkSize));
|
||||
}
|
||||
return chunkDevices;
|
||||
}
|
||||
|
||||
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||
GCM.generateGCMPayload = generateGCMPayload;
|
||||
GCM.sliceDevices = sliceDevices;
|
||||
}
|
||||
module.exports = GCM;
|
||||
@@ -14,7 +14,6 @@ var batch = require('./batch'),
|
||||
import cache from './cache';
|
||||
import Config from './Config';
|
||||
import parseServerPackage from '../package.json';
|
||||
import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
import requiredParameter from './requiredParameter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
@@ -46,6 +45,7 @@ import { setFeature } from './features';
|
||||
import { UserController } from './Controllers/UserController';
|
||||
import { UsersRouter } from './Routers/UsersRouter';
|
||||
|
||||
import ParsePushAdapter from 'parse-server-push-adapter';
|
||||
// Mutate the Parse object to add the Cloud Code handlers
|
||||
addParseCloud();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user